Author Archives: misohena

2014-10-17 ,

gameboot.js & gamescreen.js

JavaScriptで作られたゲームみたいなコンテンツをhtmlに埋め込むときに便利なライブラリを作りました。

http://github.com/misohena/js_gamescreen/

ゲームなどのコンテンツをWebページに貼り付けるとき、Youtube動画を埋め込むみたいにプレイボタンを押してから開始するようにしたくありませんか? プレビュー(サムネイル)画像を用意するのは仕方ないとして、いちいちプレイボタン画像を用意するのは面倒ですよね? ページを軽くするためにJavaScriptはボタンを押してから読み込むようにしたいですよね。読み込み終わるまでグルグルアニメーションしたいですよね。必要な.jsを読み込み終わったらプレビュー用の要素と実際のコンテンツの要素を交換したいですよね。そんな場合にgameboot.jsを使います。

埋め込んだコンテンツをウィンドウ内で最大化表示したり、全画面化(フルスクリーン化)したりしたいですよね? ゲームには色々UIが必要になってきますが、簡単なゲームだと出来合いのメニューバーみたいなものがあるとなんだかんだ言って楽ですよね。Youtubeの画面下部にあるようなコントロールバーがあれば便利ですよね? そんな場合にgamescreen.jsを使います。


(キーボード←,→,スペースを使用。フォーカス注意)

2014-10-08 ,

WiX Toolsetによるインストーラのひな形

インストーラの話題をしたところなので、WiX ToolsetでWindows Installer(.msi)を作るためのMyテンプレートなどを一つ。

ディレクトリ構成は次のような感じで。

WiX Toolsetをダウンロードしてパスを通し、上のディレクトリ構成でbuild.batを実行するとoutput/DISK1の中にインストーラが出力されます。

build.bat

ビルドするためのバッチファイルです。 出力ディレクトリの作成、wxsのコンパイル、wixobjのリンクを行います。

mkdir output
mkdir output\DISK1

candle hoge.wxs -o output/hoge.wixobj
candle MyExitDialog.wxs -o output/MyExitDialog.wixobj
candle MyInstallDirDlg.wxs -o output/MyInstallDirDlg.wixobj
candle MyUI_InstallDir.wxs -o output/MyUI_InstallDir.wixobj

light output/hoge.wixobj ^
  output/MyExitDialog.wixobj ^
  output/MyInstallDirDlg.wixobj ^
  output/MyUI_InstallDir.wixobj ^
  -ext WixUIExtension ^
  -ext WixUtilExtension ^
  -o output/DISK1/hoge.msi ^
  -pdbout output/hoge.wixpdb ^
  -cultures:ja-jp

hoge.wxs

メインとなるソースファイルです。

{YOUR-GUID}のところは全て個別のGUIDを生成して置き換えてください。

ファイル先頭ではプリプロセッサ変数を定義しています。この変数の値によって、生成されるmsiが色々変わるようになってます。

<!– Files –>と書いてある部分はheatで生成しても良いと思います。

<?xml version="1.0" encoding="utf-8"?>

<?define ProductName = "製品名" ?>
<?define Manufacturer = "製造者" ?>
<?define SrcDir = "$(sys.CURRENTDIR)src\" ?>
<?define MainExeFileName = "hoge.exe" ?>
<?define ReadmeFileName = "readme.txt" ?>
<?define UseCustomUI = "yes" ?>
<?define AppRegKey = "Software\$(var.Manufacturer)\$(var.ProductName)" ?>
<?define Compressed = "no" ?>

<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
  <Product Id="{YOUR-GUID}"
           UpgradeCode="{YOUR-GUID}"
           Name="$(var.ProductName)"
           Manufacturer="$(var.Manufacturer)"
           Version="1.0.0.0"
           Language="1041" Codepage="932">
    <Package InstallerVersion="200" Compressed="$(var.Compressed)" />

    <?if $(var.Compressed)="yes" ?>
    <MediaTemplate EmbedCab="yes" />
    <?else ?>
    <Media Id="1" DiskPrompt="DISK1" />
    <Property Id="DiskPrompt" Value="インストールディスク [1]" />
    <?endif ?>

    <Icon Id="MainIcon" SourceFile="$(var.SrcDir)$(var.MainExeFileName)" />
    <Property Id="ARPPRODUCTICON" Value="MainIcon" />

    <!-- Directory Structure -->
    <Directory Id="TARGETDIR" Name="SourceDir">
      <!-- INSTALLDIR -->
      <Directory Id="ProgramFilesFolder" Name="ProgramFiles">
        <Directory Id="ProgramFilesManufacturer" Name="$(var.Manufacturer)">
          <Directory Id="INSTALLDIR" Name="$(var.ProductName)">
          </Directory>
        </Directory>
      </Directory>
      <!-- Start Menu -->
      <Directory Id="ProgramMenuFolder" Name="Programs">
        <Directory Id="AppStartMenuDir" Name="$(var.Manufacturer) $(var.ProductName)">
        </Directory>
      </Directory>
      <!-- Desktop -->
      <Directory Id="DesktopFolder" Name="Desktop">
      </Directory>
    </Directory>

    <!-- Feature -->
    <Feature Id="EssentialFeature" Level="1">
      <ComponentGroupRef Id="EssentialFiles" />
      <ComponentRef Id="EssentialRegistries" />
      <ComponentRef Id="EssentialShortcuts" />
      <ComponentRef Id="EssentialDesktopShortcut" />
    </Feature>

    <!-- Files -->
    <ComponentGroup Id="EssentialFiles" Directory="INSTALLDIR">
      <Component Id="file0001" Guid="{YOUR-GUID}">
        <File Id="file0001" KeyPath="yes" Source="$(var.SrcDir)$(var.MainExeFileName)" />
      </Component>
      <Component Id="file0002" Guid="{YOUR-GUID}">
        <File Id="file0002" KeyPath="yes" Source="$(var.SrcDir)hoge.dat" />
      </Component>
      <Component Id="file0003" Guid="{YOUR-GUID}">
        <File Id="file0003" KeyPath="yes" Source="$(var.SrcDir)$(var.ReadmeFileName)" />
      </Component>
    </ComponentGroup>

    <!-- Shortcuts -->
    <DirectoryRef Id="AppStartMenuDir">
      <Component Id="EssentialShortcuts" Guid="{YOUR-GUID}">
        <RegistryValue Root="HKCU" Key="$(var.AppRegKey)" Name="InstalledStartMenuShortcut" Type="integer" Value="1" KeyPath="yes" />
        <RemoveFolder Id="AppStartMenuDir" On="uninstall" />
        <Shortcut Id="startMenuShortcut0001" Name="$(var.ProductName)" Target="[INSTALLDIR]$(var.MainExeFileName)" WorkingDirectory="INSTALLDIR" />
      </Component>
    </DirectoryRef>
    <Property Id="INSTALLDESKTOPSHORTCUT" Value="1" />
    <DirectoryRef Id="DesktopFolder">
      <Component Id="EssentialDesktopShortcut" Guid="{YOUR-GUID}">
        <Condition>INSTALLDESKTOPSHORTCUT</Condition>
        <RegistryValue Root="HKCU" Key="$(var.AppRegKey)" Name="InstalledDesktopShortcut" Type="integer" Value="1" KeyPath="yes" />
        <Shortcut Id="desktopShortcut0001" Name="$(var.ProductName)" Target="[INSTALLDIR]$(var.MainExeFileName)" WorkingDirectory="INSTALLDIR" />
      </Component>
    </DirectoryRef>

    <!-- Registries -->
    <DirectoryRef Id="INSTALLDIR">
      <Component Id="EssentialRegistries" Guid="{YOUR-GUID}">
        <RegistryKey Root="HKCU" Key="$(var.AppRegKey)" ForceCreateOnInstall="yes" ForceDeleteOnUninstall="yes">
          <RegistryValue Type="string" Name="InstalledPath" Value="[INSTALLDIR]" KeyPath="yes" />
        </RegistryKey>
      </Component>
    </DirectoryRef>

    <!-- User Interface -->
    <!-- UI: Install Directory -->
    <Property Id="WIXUI_INSTALLDIR" Value="INSTALLDIR" />

    <!-- UI: Bitmap -->
    <WixVariable Id="WixUIBannerBmp" Value="banner.bmp" /><!-- 493x58 -->
    <WixVariable Id="WixUIDialogBmp" Value="dialog.bmp" /><!-- 493x312 -->

    <?if $(var.UseCustomUI) = "yes" ?>
      <UIRef Id="MyUI_InstallDir" />
      <!-- UI: Run application after installed (for MyUI_InstallDir) -->
      <?if $(var.MainExeFileName) And $(var.MainExeFileName)!="" ?>
        <Property Id="MYUI_EXITDIALOGOPTIONALCHECKBOXTEXT" Value="プログラムを実行する" />
        <Property Id="MYUI_EXITDIALOGOPTIONALCHECKBOX" Value="1" />
        <CustomAction Id="LaunchApplication" Directory="INSTALLDIR" ExeCommand="[INSTALLDIR]$(var.MainExeFileName)" Return="asyncNoWait" />
        <UI>
          <Publish Dialog="MyExitDialog" Control="Finish" Event="DoAction" Value="LaunchApplication">MYUI_EXITDIALOGOPTIONALCHECKBOX = 1 and NOT Installed</Publish>
        </UI>
      <?endif ?>
      <?if $(var.ReadmeFileName) And $(var.ReadmeFileName)!="" ?>
        <Property Id="MYUI_EXITDIALOGOPTIONALCHECKBOXTEXT2" Value="Readmeを開く" />
        <Property Id="MYUI_EXITDIALOGOPTIONALCHECKBOX2" Value="1" />
        <Property Id="WixShellExecTarget" Value="[INSTALLDIR]$(var.ReadmeFileName)" />
        <CustomAction Id="LaunchReadme" BinaryKey="WixCA" DllEntry="WixShellExec" Impersonate="yes" />
        <UI>
          <Publish Dialog="MyExitDialog" Control="Finish" Event="DoAction" Value="LaunchReadme">MYUI_EXITDIALOGOPTIONALCHECKBOX2 = 1 and NOT Installed</Publish>
        </UI>
      <?endif ?>
      <!-- UI: Skip license (for MyUI_InstallDir) -->
      <UI>
        <Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="MyInstallDirDlg">1</Publish>
        <Publish Dialog="MyInstallDirDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg">1</Publish>
      </UI>

    <?else ?>

      <UIRef Id="WixUI_InstallDir" />
      <!-- UI: Run application after installed (for WixUI_InstallDir) -->
      <Property Id="WIXUI_EXITDIALOGOPTIONALCHECKBOXTEXT" Value="プログラムを実行する" />
      <Property Id="WIXUI_EXITDIALOGOPTIONALCHECKBOX" Value="1" />
      <CustomAction Id="LaunchApplication" Directory="INSTALLDIR" ExeCommand="[INSTALLDIR]$(var.MainExeFileName)" Return="asyncNoWait" />
      <UI>
        <Publish Dialog="ExitDialog" Control="Finish" Event="DoAction" Value="LaunchApplication">WIXUI_EXITDIALOGOPTIONALCHECKBOX = 1 and NOT Installed</Publish>
      </UI>
      <!-- UI: Skip license (for WixUI_InstallDir) -->
      <UI>
        <Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="InstallDirDlg">1</Publish>
        <Publish Dialog="InstallDirDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg">1</Publish>
      </UI>
    <?endif ?>


  </Product>
</Wix>

MyInstallDirDlg.wxs

インストール先を指定するダイアログを定義するファイルです。

WiXのソースに含まれているファイル(src/ext/UIExtension/InstallDirDlg.wxs)をコピーして一部を改編しました。改編点は次の通りです。

  • DialogのIdをInstallDirDlgからMyInstallDirDlgへ変更しました。
  • 「デスクトップにショートカットを作成する」チェックボックスを追加しました。
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
   <Fragment>
       <UI>
           <Dialog Id="MyInstallDirDlg" Width="370" Height="270" Title="!(loc.InstallDirDlg_Title)">
               <Control Id="Next" Type="PushButton" X="236" Y="243" Width="56" Height="17" Default="yes" Text="!(loc.WixUINext)" />
               <Control Id="Back" Type="PushButton" X="180" Y="243" Width="56" Height="17" Text="!(loc.WixUIBack)" />
               <Control Id="Cancel" Type="PushButton" X="304" Y="243" Width="56" Height="17" Cancel="yes" Text="!(loc.WixUICancel)">
                   <Publish Event="SpawnDialog" Value="CancelDlg">1</Publish>
               </Control>

               <Control Id="Description" Type="Text" X="25" Y="23" Width="280" Height="15" Transparent="yes" NoPrefix="yes" Text="!(loc.InstallDirDlgDescription)" />
               <Control Id="Title" Type="Text" X="15" Y="6" Width="200" Height="15" Transparent="yes" NoPrefix="yes" Text="!(loc.InstallDirDlgTitle)" />
               <Control Id="BannerBitmap" Type="Bitmap" X="0" Y="0" Width="370" Height="44" TabSkip="no" Text="!(loc.InstallDirDlgBannerBitmap)" />
               <Control Id="BannerLine" Type="Line" X="0" Y="44" Width="370" Height="0" />
               <Control Id="BottomLine" Type="Line" X="0" Y="234" Width="370" Height="0" />

               <Control Id="FolderLabel" Type="Text" X="20" Y="60" Width="290" Height="30" NoPrefix="yes" Text="!(loc.InstallDirDlgFolderLabel)" />
               <Control Id="Folder" Type="PathEdit" X="20" Y="100" Width="320" Height="18" Property="WIXUI_INSTALLDIR" Indirect="yes" />
               <Control Id="ChangeFolder" Type="PushButton" X="20" Y="120" Width="56" Height="17" Text="!(loc.InstallDirDlgChange)" />

               <!-- Begin MyUI -->
               <Control Id="DesktopShortcutCheckBox" Type="CheckBox" X="20" Y="160" Width="290" Height="17" Property="INSTALLDESKTOPSHORTCUT" CheckBoxValue="1" Text="デスクトップにショートカットを作成する。" />
               <!-- End MyUI -->
           </Dialog>
       </UI>
   </Fragment>
</Wix>

MyExitDialog.wxs

インストールが終了したときに表示するダイアログを定義するファイルです。

WiXのソースに含まれているファイル(src/ext/UIExtension/ExitDialog.wxs)をコピーして一部を改編しました。改編点は次の通りです。

  • DialogのIdをExitDialogからMyExitDialogへ変更しました。
  • チェックボックスを二つ表示できるようにしました。 アプリの起動用とreadmeの表示用です。 なお、チェックボックスのText=をそのまま使うと背景がグレーになって見た目が悪いので、チェックボックスを最小限のサイズで表示して、その横に新たにテキストを配置することで背景が透過するようにしています。副作用として、テキスト部分を押してもチェックボックスが反応しなくなってしまいますが、そこは妥協しています(InstallShieldが作るmsiも同じことをしているせいで押しにくい)。
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
   <Fragment>
       <UI>
           <Dialog Id="MyExitDialog" Width="370" Height="270" Title="!(loc.ExitDialog_Title)">
               <Control Id="Finish" Type="PushButton" X="236" Y="243" Width="56" Height="17" Default="yes" Cancel="yes" Text="!(loc.WixUIFinish)" />
               <Control Id="Cancel" Type="PushButton" X="304" Y="243" Width="56" Height="17" Disabled="yes" Text="!(loc.WixUICancel)" />
               <Control Id="Bitmap" Type="Bitmap" X="0" Y="0" Width="370" Height="234" TabSkip="no" Text="!(loc.ExitDialogBitmap)" />
               <Control Id="Back" Type="PushButton" X="180" Y="243" Width="56" Height="17" Disabled="yes" Text="!(loc.WixUIBack)" />
               <Control Id="BottomLine" Type="Line" X="0" Y="234" Width="370" Height="0" />
               <Control Id="Description" Type="Text" X="135" Y="70" Width="220" Height="40" Transparent="yes" NoPrefix="yes" Text="!(loc.ExitDialogDescription)" />
               <Control Id="Title" Type="Text" X="135" Y="20" Width="220" Height="60" Transparent="yes" NoPrefix="yes" Text="!(loc.ExitDialogTitle)" />

               <!-- Begin MyUI -->
               <Control Id="OptionalText" Type="Text" X="135" Y="110" Width="220" Height="80" Transparent="yes" NoPrefix="yes" Hidden="yes" Text="[MYUI_EXITDIALOGOPTIONALTEXT]">
                   <Condition Action="show">MYUI_EXITDIALOGOPTIONALTEXT AND NOT Installed</Condition>
               </Control>
               <Control Id="OptionalCheckBox" Type="CheckBox" X="150" Y="150" Width="10" Height="9" Hidden="yes" Property="MYUI_EXITDIALOGOPTIONALCHECKBOX" CheckBoxValue="1">
                   <Condition Action="show">MYUI_EXITDIALOGOPTIONALCHECKBOXTEXT AND NOT Installed</Condition>
               </Control>
               <Control Id="OptionalCheckBoxText" Type="Text" X="165" Y="150" Width="200" Height="13" Transparent="yes" Hidden="yes" Text="[MYUI_EXITDIALOGOPTIONALCHECKBOXTEXT]">
                   <Condition Action="show">MYUI_EXITDIALOGOPTIONALCHECKBOXTEXT AND NOT Installed</Condition>
               </Control>
               <Control Id="OptionalCheckBox2" Type="CheckBox" X="150" Y="180" Width="10" Height="9" Hidden="yes" Property="MYUI_EXITDIALOGOPTIONALCHECKBOX2" CheckBoxValue="1">
                   <Condition Action="show">MYUI_EXITDIALOGOPTIONALCHECKBOXTEXT2 AND NOT Installed</Condition>
               </Control>
               <Control Id="OptionalCheckText2" Type="Text" X="165" Y="180" Width="200" Height="13" Transparent="yes" Hidden="yes" Text="[MYUI_EXITDIALOGOPTIONALCHECKBOXTEXT2]">
                   <Condition Action="show">MYUI_EXITDIALOGOPTIONALCHECKBOXTEXT2 AND NOT Installed</Condition>
               </Control>
               <!-- End MyUI -->
           </Dialog>

           <InstallUISequence>
               <Show Dialog="MyExitDialog" OnExit="success" Overridable="yes" />
           </InstallUISequence>

           <AdminUISequence>
               <Show Dialog="MyExitDialog" OnExit="success" Overridable="yes" />
           </AdminUISequence>
       </UI>
   </Fragment>
</Wix>

MyUI_InstallDir.wxs

UIの流れを定義するファイルです。

WiXのソースに含まれているファイル(src/ext/UIExtension/WixUI_InstallDir.wxs)をコピーして一部を改編しました。上で改編したダイアログを使うように、一部のダイアログIdにMyを付加しました。

<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
   <Fragment>
       <UI Id="MyUI_InstallDir">
           <TextStyle Id="WixUI_Font_Normal" FaceName="Tahoma" Size="8" />
           <TextStyle Id="WixUI_Font_Bigger" FaceName="Tahoma" Size="12" />
           <TextStyle Id="WixUI_Font_Title" FaceName="Tahoma" Size="9" Bold="yes" />

           <Property Id="DefaultUIFont" Value="WixUI_Font_Normal" />
           <Property Id="WixUI_Mode" Value="InstallDir" />

           <DialogRef Id="BrowseDlg" />
           <DialogRef Id="DiskCostDlg" />
           <DialogRef Id="ErrorDlg" />
           <DialogRef Id="FatalError" />
           <DialogRef Id="FilesInUse" />
           <DialogRef Id="MsiRMFilesInUse" />
           <DialogRef Id="PrepareDlg" />
           <DialogRef Id="ProgressDlg" />
           <DialogRef Id="ResumeDlg" />
           <DialogRef Id="UserExit" />
           
           <Publish Dialog="BrowseDlg" Control="OK" Event="DoAction" Value="WixUIValidatePath" Order="3">1</Publish>
           <Publish Dialog="BrowseDlg" Control="OK" Event="SpawnDialog" Value="InvalidDirDlg" Order="4"><![CDATA[WIXUI_INSTALLDIR_VALID<>"1"]]></Publish>

           <Publish Dialog="MyExitDialog" Control="Finish" Event="EndDialog" Value="Return" Order="999">1</Publish>

           <Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="LicenseAgreementDlg">NOT Installed</Publish>
           <Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg">Installed AND PATCH</Publish>

           <Publish Dialog="LicenseAgreementDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg">1</Publish>
           <Publish Dialog="LicenseAgreementDlg" Control="Next" Event="NewDialog" Value="MyInstallDirDlg">LicenseAccepted = "1"</Publish>

           <Publish Dialog="MyInstallDirDlg" Control="Back" Event="NewDialog" Value="LicenseAgreementDlg">1</Publish>
           <Publish Dialog="MyInstallDirDlg" Control="Next" Event="SetTargetPath" Value="[WIXUI_INSTALLDIR]" Order="1">1</Publish>
           <Publish Dialog="MyInstallDirDlg" Control="Next" Event="DoAction" Value="WixUIValidatePath" Order="2">NOT WIXUI_DONTVALIDATEPATH</Publish>
           <Publish Dialog="MyInstallDirDlg" Control="Next" Event="SpawnDialog" Value="InvalidDirDlg" Order="3"><![CDATA[NOT WIXUI_DONTVALIDATEPATH AND WIXUI_INSTALLDIR_VALID<>"1"]]></Publish>
           <Publish Dialog="MyInstallDirDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg" Order="4">WIXUI_DONTVALIDATEPATH OR WIXUI_INSTALLDIR_VALID="1"</Publish>
           <Publish Dialog="MyInstallDirDlg" Control="ChangeFolder" Property="_BrowseProperty" Value="[WIXUI_INSTALLDIR]" Order="1">1</Publish>
           <Publish Dialog="MyInstallDirDlg" Control="ChangeFolder" Event="SpawnDialog" Value="BrowseDlg" Order="2">1</Publish>
           
           <Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="MyInstallDirDlg" Order="1">NOT Installed</Publish>
           <Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="MaintenanceTypeDlg" Order="2">Installed AND NOT PATCH</Publish>
           <Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg" Order="2">Installed AND PATCH</Publish>

           <Publish Dialog="MaintenanceWelcomeDlg" Control="Next" Event="NewDialog" Value="MaintenanceTypeDlg">1</Publish>

           <Publish Dialog="MaintenanceTypeDlg" Control="RepairButton" Event="NewDialog" Value="VerifyReadyDlg">1</Publish>
           <Publish Dialog="MaintenanceTypeDlg" Control="RemoveButton" Event="NewDialog" Value="VerifyReadyDlg">1</Publish>
           <Publish Dialog="MaintenanceTypeDlg" Control="Back" Event="NewDialog" Value="MaintenanceWelcomeDlg">1</Publish>

           <Property Id="ARPNOMODIFY" Value="1" />
       </UI>

       <UIRef Id="WixUI_Common" />
   </Fragment>
</Wix>

ソース一式

ソース一式は github/simple_wix_template に置いてあります。

2014-10-07 ,

Windows InstallerがKB2918614で遅くなった件

InstallShieldで作ったインストーラが信じられないくらい遅いので調べたところ、どうもセキュリティ更新プログラム KB2918614 に問題があるようなのです。

作ったインストーラはDVD-ROMから4GBくらいインストールするのですが、インストールを開始してすぐ、準備段階でプログレスバーが止まったまま10分程度まったく動かなくなります。それを過ぎるとファイルのコピーが始まり、最終的には正常にインストールが完了するのですが、以前はこんなこと起きなかったのにどうなっているんだろうと思い色々調べてみました。

タスクマネージャでmsiexec.exe(.msiを実際に実行するプロセス)のI/O読み取りバイト数を見てみると、止まっている間でもガンガン何かを読み込んでいることが分かります。4GBくらい読み込んだところでプログレスバーが動き始めコピーをしていると表示があり、また4GBくらい読み込むとインストールが終了します。インストールするファイル(もちろん無圧縮、アーカイブ無し)を計2回も読み込んでいるようなのです。そりゃ遅いはずです。それもそのうち最初の1回はプログレスバーが全く動きません。DVD-ROMから4GB読み込むのに10分くらいかかりますから、その間止まっているように見えるわけです。

試しにHDDにインストーラをコピーしてから実行したり、新しくWiX Toolsetでmsiを作ってみたりしましたが、やはりWindows Installerを使う限り二回読み込むことには変わりないようです。

同じインストーラの作り方で以前はこのようなことは起きませんでした。なので以前作ったインストーラを調べてみたのですが、今実行するととてつもなく遅い! 同じ問題が起きています。これはOS側、Windows Installer側に何かあったのでは無いか、と思いました。

Webで色々調べているとKB2918614という単語を見かけました。

【至急】KB2918614適用後、アプリケーションの画像データのインストールに非常に時間がかかるようになった - マイクロソフト コミュニティ

一般コンシューマ向けパッケージソフト製品開発を行っている企業で、開発者をしております。

8/13のWindows UpdateでKB2918614適用後、以下のようなコンポーネントのインストールに非常に時間がかかるようになってしまいました。

環境によっては数時間同じ画面で停止しているという状況になりますので、エンドユーザーから見るとインストールが停止したような形になります。

おそらくは問い合わせが多数寄せられると思いますので、まずはユーザーへの対応策を検討する必要があります。

..略…

膨大なロットを市場に出荷しており、発売直後に第1の売り上げのピークを記録するというソフトウェア製品の性格上、

対応次第ではかなりの損害が出てしまいかねない状況ですので、大至急ご確認をお願いいたします。

なかなか生々しいですね……。

KB2918614MS14-049の脆弱性を解決するためのセキュリティ更新プログラムのようです。出たのは今年の8月。

筆まめやウィルスバスターなど、いくつかの製品のサポート情報では対策としてKB2918614のアンインストールを挙げています。

その他Twitterを検索してみると8月にKB2918614が出て以降、様々なアプリケーションがインストール不能に陥っているようですね。

試しにKB2918614をアンインストールして再度インストーラの動きを確認してみました。するとすぐにコピーが始まりますし、msiexec.exeは1回分(4GB)しか読み込みません。再度KB2918614を入れると遅くなります。KB2918614の有無で動作が変わるのは間違いないようです。

msiexec /L*v log.txt /i hoge.msi としてログを取ってみましたが、KB2918614が入っているときは止まって見える間に SECREPAIR: CryptAcquireContext succeeded のようにSECREPAIRという単語を含むログが出力されます。KB2918614が入っていないときは出力されません。セキュリティ上の確認処理か何かをしているのでしょうか。

結局対策としては、

  1. エラーが出ない限り待てばインストールは完了する
  2. 待てない場合はKB2918614をアンインストールするか手動でインストール
  3. エラーが出る場合もKB2918614をアンインストールするか手動でインストール

ということですね。

セキュリティ更新プログラムなのでアンインストールするとセキュリティ上の問題MS14-049が残ってしまうので注意が必要だと思います。

KB2918614で挙げられている既知の問題について、マイクロソフトは現在調査中としています。「遅い」というのは既知の問題として挙げられていませんが、近いうちに修正されることを祈るしか無いでしょう。修正されなければWindows Installerは死んだも同然でしょうね。プログレスバーが数十分も止まったままになるインストーラなんて使い物になりません。msiではない古き良きスクリプト駆動型インストーラに戻るしかありません。

2014-10-01 , ,

Org2blogの設定

Org2blogの設定をしました。

下のelispで次のようなことをします。

  • M-x blog-newで投稿用バッファを作成します。
  • 投稿用バッファでC-x C-sしたときにファイル名を自動設定します。まだファイル名が決まっていないときに限り、ブログ用ディレクトリ設定や記事中のDATE、PERMALINK、TITLEからファイル名を生成し、ファイル名を設定するか尋ねます。
  • ブログ用ディレクトリ下のorgファイルを開くとき、org2blog/wp-modeマイナーモードを有効にします。
  • .org2blog.orgの保存先をブログ用ディレクトリにします。
  • ブログのテンプレートにPERMALINKを入れます。
;; ブログorgファイルのセーブ先
(setq my-blog-directory "~/org/blog/")

;; 投稿設定
(setq org2blog/wp-blog-alist
      `(("example"
         :url "http://example.jp/xmlrpc.php"
         :username "example-user"
         :default-title "NewEntry"
         :default-categories nil ;カテゴリーはデフォルト
         :track-posts (,(concat my-blog-directory ".org2blog.org") "Posts") ;.org2blogの保存先を変える
)))

(setq org2blog/wp-default-categories '()) ;カテゴリーは使わないので空

(setq org2blog/wp-buffer-template
      "#+DATE: %s
#+OPTIONS: toc:nil num:nil todo:nil pri:nil tags:nil ^:nil
#+CATEGORY: %s
#+TAGS: 
#+PERMALINK: 
#+TITLE: %s
\n") ;必ずPERMALINKを入れる


;; セーブ時のファイル名生成

(defun my-blog-get-buffer-post-file-name ()
  "現在のバッファのファイル名を作成します。directory/YYYY-MM-DD-permalink_or_title.orgの形式です。directoryはmy-blog-directory変数を使います。"
  (let* ((date (org2blog/wp-get-option "DATE"))
         (title (org2blog/wp-get-option "TITLE"))
         (permalink (org2blog/wp-get-option "PERMALINK"))
         (filename-date (format-time-string "%Y-%m-%d"
                                            (if date (apply #'encode-time (org-parse-time-string date))
                                              (current-time)))))
    (concat my-blog-directory filename-date "-" (if (> (length permalink) 0) permalink title) ".org")))

(defun my-blog-set-buffer-file-name ()
  "デフォルトのファイル名をバッファに設定します。"
  (if (not (buffer-file-name))
      (let ((filename (my-blog-get-buffer-post-file-name)))
        (if (y-or-n-p (format "set filename to '%s'?" filename))
            (set-visited-file-name filename)))))

(defun my-blog-save ()
  "バッファをセーブします。まだバッファにファイル名が設定されていないとき、セーブする前にデフォルトのファイル名を設定するかどうかを訪ねます。"
  (interactive)
  (my-blog-set-buffer-file-name)
  (save-buffer))

(defun blog-new ()
  "ブログの新しいエントリーを作成します。"
  (interactive)
  (org2blog/wp-new-entry)
  (local-set-key "\C-x\C-s" 'my-blog-save))

;; ブログディレクトリ下のファイルを開くときはorg2blogを有効にする。
(add-hook
 'org-mode-hook
 (lambda ()
   (if (and (buffer-file-name)
            (string-prefix-p (expand-file-name my-blog-directory) (buffer-file-name)))
       (org2blog/wp-mode t))))

最初はorg2blog/wp-kill-buffer-hookをdefadviceで書き換えたりしたのですが、いくつかのケースでうまく動かなかったためやめておきました。元々少し不具合があるみたいですし、ちゃんとやるならもっと色々手を入れないとダメそう。

2014-09-30

タブレットで読書

最近10.1インチのAndroidタブレット(Xperia Z2 Tablet)で自炊した本を読んでます。

大きいタブレットだと色々はかどりますね。バッテリーも持ちますし。読むのに疲れたら何か他のものを見たりもできますし(Nexus5でテザリング)。何より版面が見やすいです。

ただ、本のサイズによっては両開きだと文字が小さく、片面だと逆に大きすぎるという場合もあります。今読んでいるのは元がA5サイズですが、まさにそんな感じ。少し文字が小さくなりますが両開きで読んでます。

アプリはezPDF Readerを使ってます。ちゃんとPDFの閉じ方向情報を認識してくれることが必須ですね。両開きで表示したときにページの左右が逆になってしまいますので。余白のカット(ページの切り抜き)表示もできます。

家でも外でも何冊もの本が同じように読めるの良いです。最近は未読の本も有用な本もどんどんスキャンしているのでライブラリも充実してきました。

ちなみに今読んでいるのはアナキズム・イン・ザ・UK -壊れた英国とパンク保育士奮闘記。内容はこことかこことか参照。Life Is A Piece Of Shit ―人生は一片のクソなんだよ。

2014-09-26 ,

JekyllからWordPressへ移行

JekyllからWordpressへ移行しました。

Jekyllは生成が遅すぎて耐えきれなかったというのが第一の理由です。一つ記事を書き終わってから更新をかけると、ほぼすべてのページを再生成するため、どうしても数十秒かかります。すべてのページを再生成しなくても良いように思うかもしれませんが、各ページにあるナビゲーションリンクやタグごとの記事数なんかを書き直さなければなりません。このサイトには1500くらい記事があるのですが1分くらいはかかります。関連記事や記事数といった情報はいったん全部の記事を読み込んでからで無いと求まりませんし。incrementalな再生成も検討されているようなのですが、できるのはまだ先のようなので。

さくらのレンタルサーバで簡単にWordpressを入れる方法が紹介されていたのも移行を後押ししました。

外出先からなど、もっと自由に記事を書きたかったですし。

移行手順は次のような感じでした。

記事の変換はいくつかハマったところがありました。

まずはインポートできる形式を調べたのですが、めぼしいものが見当たりません。結局WordPress間でやりとりするのに使われるであろう形式にこれまでの記事を変換することにしました。
これまでの記事はJekyllのyamlヘッダー付きhtml形式です(昔の独自マークアップ形式やorg-mode形式からhtmlへ変換されたものです)。それをWordpressでインポートできるWXR(WordPress eXtended RSS)へ変換しました。変換に使ったEmacs Lispは次の通り。

(defun jtow-parse-html (file)
  "yamlヘッダー付きhtmlファイルを解析して(記事URL slug タイトル 時刻 タグ文字列リスト 内容文字列)を返します。"
  (with-temp-buffer
    (insert-file file)
    ;; ---
    (beginning-of-buffer)
    (if (not (looking-at "^---\n")) (error "syntax error. '---'"))
    ;; yaml header
    (next-line)
    (let ((yaml-header nil))
      (while (not (looking-at "^---"))
        (if (not (looking-at "^\\([^:]+\\): *\\(.*\\)$"))
            (error "syntax error. %s %s" file (buffer-substring (point-at-bol) (point-at-eol))))
        (push (cons (match-string-no-properties 1) (match-string-no-properties 2)) yaml-header)
        (next-line))
      (next-line)

      ;; check
      (let ((title (cdr (assoc "title" yaml-header)))
            (date (cdr (assoc "date" yaml-header)))
            (tags (cdr (assoc "tags" yaml-header)))
            (content (buffer-substring-no-properties (point) (point-max))))
        (if (not title) (error "title not found. %s" file))
        (if (not date) (error "date not found. %s" file))

        ;; result
        (list
         ;; link
         (concat "http://example.jp/blog/" file)
         ;; postname
         (if (string-match "^[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]-\\([^.]+\\)\\.html$" file ) (match-string 1 file) nil)
         ;; title
         (substring title 1 -1)
         ;; date
         (jtow-parse-date date)
         ;; tags
         (if tags (split-string tags " " t))
         ;; content
         content))
      )))

(defun jtow-parse-date (d)
  "2014-01-02という形式の文字列からEmacsの時刻形式へ変換します。"
  (let ((tm (parse-time-string d)))
    (encode-time 0 0 0 (nth 3 tm) (nth 4 tm) (nth 5 tm) (* 9 3600))))

(defun jtow-format-date-rfc2822 (time)
  (format-time-string "%a, %d %b %Y 00:00:00 %z" time))

(defun jtow-format-time (time &optional universal)
  (format-time-string "%Y-%m-%d %H:%M:%S" time universal))

(defun jtow-make-item (item)
  "jtow-parse-htmlが返したリストをWXRのitem要素へ変換します。"
  (let ((link (nth 0 item))
        (postname (nth 1 item))
        (title (nth 2 item))
        (date (nth 3 item))
        (tags (nth 4 item))
        (content (nth 5 item)))
    (concat
     "<item>"
     (if link (concat "<link>" link "</link>\n"))
     (if title (concat "<title>" title "</title>\n"))
     (if date (concat "<pubDate>" (jtow-format-date-rfc2822 date) "</pubDate>\n"))
     (if tags (loop for tag in tags concat (format "<category domain=\"post_tag\" nicename=\"%s\"><![CDATA[%s]]></category>\n" (downcase tag) tag)))

     (format "<wp:post_date>%s</wp:post_date>\n" (jtow-format-time date))
     (format "<wp:post_date_gmt>%s</wp:post_date_gmt>\n" (jtow-format-time date t))
     (concat "<wp:post_name>" postname "</wp:post_name>\n")
     "<wp:post_type>post</wp:post_type>\n"
     "<wp:status>publish</wp:status>\n"

     ;; content
     "<content:encoded><![CDATA[" content "]]></content:encoded>\n"
     "</item>\n")))

(defun jtow-dir-html-to-xml (dir outfile)
  "ディレクトリ内のyamlヘッダー付きhtmlファイルからWXRファイルを作成します。"
  (with-temp-file outfile
    (insert
     "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>
<rss version=\"2.0\"
	xmlns:excerpt=\"http://wordpress.org/export/1.2/excerpt/\"
	xmlns:content=\"http://purl.org/rss/1.0/modules/content/\"
	xmlns:wfw=\"http://wellformedweb.org/CommentAPI/\"
	xmlns:dc=\"http://purl.org/dc/elements/1.1/\"
	xmlns:wp=\"http://wordpress.org/export/1.2/\"
>
<channel>
	<title>Example Blog</title>
	<link>http://example.jp/blog</link>
	<description></description>
	<pubDate>Thu, 25 Sep 2014 05:56:52 +0000</pubDate>
	<language>ja</language>
	<wp:wxr_version>1.2</wp:wxr_version>
	<wp:base_site_url>http://example.jp/blog</wp:base_site_url>
	<wp:base_blog_url>http://example.jp/blog</wp:base_blog_url>

	<wp:author>
	<wp:author_id>1</wp:author_id>
	<wp:author_login>foo</wp:author_login>
	<wp:author_email>foo@example.jp</wp:author_email>
	<wp:author_display_name><![CDATA[foo]]></wp:author_display_name>
	<wp:author_first_name><![CDATA[]]></wp:author_first_name>
	<wp:author_last_name><![CDATA[]]></wp:author_last_name>
	</wp:author>

	<generator>http://wordpress.org/?v=4.0</generator>
")

    (loop for file in (directory-files dir nil "\.html$") do (insert (jtow-make-item (jtow-parse-html file))))

    (insert "</channel></rss>")))

;; ex)カレントディレクトリにある.htmlからposts.xmlを作る。
;;(jtow-dir-html-to-xml "." "posts.xml")

私が使っていたyamlヘッダーは主にtags, date, titleだけでしたので、それだけ考慮してあります。

CDATAの障害になる]]>という文字列が使われていないことも確認しました。

一番困ったのが、slug(postname)が重複してはいけないということでした。昔の記事では/blog/2006-01-01-a.htmlのようなURLが多いのですが、aの部分は他の日付でも沢山使われているので重複してしまいます。重複すると自動的にa-2、a-3のように補正されてしまいます。仕方ないので/blog/2006-01-01-060101a.htmlのように確実にユニークになるようにファイル名を置換しました。以前のURLと変わってしまいますが諦めました。

外観のカスタマイズはBootpressテーマをベースにして、気に入らないところをひたすら書き換えては表示してみる繰り返しでした。
それにしてもphpは久しぶりです。しかしまぁ、見えないグローバル変数に依存するコードが多くて分かりづらいですね。WP_Queryが割と万能な模様。

一通りカスタマイズが終わったらテスト用ディレクトリから本番ディレクトリへ移し替えて完成です。

投稿は今まで事実上Emacsからしか出来ませんでしたが、これからはブラウザ、Androidアプリ、Emacsと色々なところから出来るようになりました。

AndroidのWordPressアプリは面白いですね。写真を撮ったそばから投稿できます。

Emacsからの投稿はOrg2blogを導入。el-get経由でインストールしました。bzrが実行できないと言われたのでCygwinのインストーラからbzrをインストール(xml-rpc-elのインストールに必要)。でもWindowsのEmacsを使っていて、かつ、bzrはPythonスクリプトなのでEmacsからexecutable-findで見つからず。bzr.batを作ってsh -c '/bin/bzr %*'のようにしてなんとかel-getからインストールできました。(MELPAをpackageに追加してそこからインストールした方が良かったかも)

Org2blogもAndroidのWordPressアプリもですがslugが入力できません。URLが日本語になってしまいますが、まぁ、これも諦めるしかないでしょうか。(Org2blogでは#+PERMALINKでslugを設定できました)