2012-09-23

perl - windows の電源コントロール

コマンドプロンプトのみでサスペンド(スリープ)は、できないみたいなので perl で挑戦。

PowrProf.dll と shutdown.exe を呼んで
サスペンド、ハイバネート、シャットダウン、リブート、ログオフを
n 秒後に実行できるようにしてみました。

サスペンド後にタスクスケジューラでの復帰もできます。(後述)
テスト環境は winXP SP3、Activeperl 5.14。

#使用例

#t オプションのデフォルト値は 30秒
#カウントダウン中、CTRL+C で中止します

#60秒後にサスペンド
> pwrctrl.pl -s -t 60
> pwrctrl.pl --suspend --timeout 60

#即シャットダウン
> pwrctrl.pl -d -t 0
> pwrctrl.pl --shutdown --timeout 0

#ヘルプ
> pwrctrl.pl -?
> pwrctrl.pl --help

# pwrctrl.pl
# windows power control script

use strict;
use warnings;
use Getopt::Long;
use Win32::API;

our $VERSION = "0.02";

$| = 1;                 #no buffering
$Win32::API::DEBUG = 0; #Win32::API debug flag


my $cmd = init();

count_down($cmd);

my $return_code = $cmd->{'run'}->();

print "return code : $return_code\n";

exit;

#--------------------

sub init{
    usage() unless @ARGV;

    ###get options
    my $opts = {'timeout' => 30}; #default

    GetOptions($opts,
        'help|?',
        'hibernate|h',
        'suspend|s',
        'shutdown|d',
        'reboot',
        'logoff',
        'timeout=i',
        ) || usage();

    usage() if $opts->{'help'};


    ###set command
    my $api = Win32::API->new('Powrprof', 'SetSuspendState', 'NNN', 'N')
                || die "Can't load API : $!\n";

    my $cmd_table = {
        'hibernate'=> sub{ $api->Call(1,0,0)  },
        'suspend'  => sub{ $api->Call(0,0,0)  },
        'shutdown' => sub{ `shutdown -s -t 0` },
        'reboot'   => sub{ `shutdown -r -t 0` },
        'logoff'   => sub{ `shutdown -l -t 0` },
        };
    my $cmd = {'timeout' => $opts->{'timeout'}};

    for my $key (keys %$opts) {
        next if $key =~ /help|timeout/;
        $cmd->{'name'} = $key;
        $cmd->{'run'}  = $cmd_table->{$key};
        last;
    }

    usage(qq/option requires "-h -s -r -d -l -? [-t]"/)
        unless $cmd->{'name'};

    return $cmd;
}


sub count_down{
    my $cmd = shift;
    my $command = uc $cmd->{'name'};
    my $timeout = $cmd->{'timeout'};
    
    local $SIG{'INT'} = 'abort';

    print<<"EOM";

$command after $timeout seconds.
Press [CTRL+C] to abort.

EOM

    return if $timeout == 0;

    for my $sec (reverse (0..$timeout)) {
        printf "\r%3d sec", $sec;
        sleep 1;
        printf "\r%10s\r", "";
    }
}


sub abort {
    print "\n\n", "abort!\n";
    exit;
}


sub usage {
    my $msg = shift;
    print "$msg\n" if $msg;
    
    print<<"EOM";

- windows power control script v$VERSION -

Usage   : pwrctrl.pl option [timeout]
          pwrctrl.pl -d -t 60   # shutdown after 60sec

Options :
    -? --help         this document

    -h --hibernate    hibernate
    -s --suspend      suspend
    -r --reboot       reboot
    -d --shutdown     shutdown
    -l --logoff       logoff

    [-t] [--timeout]  run after n seconds
                      default is 30
EOM

    exit;
}

__END__


rundll32.exe だと自動復帰できない

> rundll32.exe PowrProf.dll,SetSuspendState 0,1,0

> rundll32.exe PowrProf.dll,SetSuspendState Sleep

コマンドプロンプトからサスペンドするのに上記のコードがWEB上で見つかります。
サスペンドするだけなら問題ないみたいですが、 タスクスケジューラでの自動復帰ができません。
※Windows7 だと問題なく自動復帰できるようです。


Rundll32.exe は正しい使用法なのか?

まず、Rundll32.exe の仕様を確認します。
Windows の Rundll と Rundll32 インターフェイス

これによると、Rundll32.exe は以下のように dll の関数呼び出しをする仕様です。
void CALLBACK
EntryPoint(HWND hwnd, HINSTANCE hinst, LPSTR lpszCmdLine, int nCmdShow);

hwnd        - DLL で作成する任意のウィンドウのオーナー ウィンドウとして
              使用するウィンドウ ハンドル
hinst       - DLL のインスタンス ハンドル
lpszCmdLine - DLL で解析する ASCIIZ コマンド ライン
nCmdShow    - DLL のウィンドウを表示する方法を指定

Rundll32.exe は dll の関数への引数を 文字列 として渡します。
引数は dll がパースしてねという仕様です。


次に、SetSuspendState の仕様を確認します。
SetSuspendState function - MSDN
C++
BOOLEAN WINAPI SetSuspendState(
    _In_  BOOLEAN Hibernate,
    _In_  BOOLEAN ForceCritical,
    _In_  BOOLEAN DisableWakeEvent
);
SetSuspendState の正しい引数は bool値 3つ。

1つ目は、 0 = サスペンド、 1 = ハイバネート
2つ目は、 0 = アプリケーションの終了を待って中断、 1 = 強制的に中断
3つ目は、 0 = wake イベント有効、 1 = wake イベント無効


これらを踏まえると、先の Rundll32.exe 経由の SetSuspendState 呼び出しはこうなります。
> rundll32.exe PowrProf.dll,SetSuspendState 0,1,0

SetSuspendState( hwnd, hinst, "0,1,0", nCmdShow );

引数を渡すと DisableWakeEvent の値が TRUE になるので
wake イベントで復帰できないというわけです。


つまり、Rundll32.exe 経由での SetSuspendState 呼び出しは間違ってます。


perl 以外の方法

1. SetSuspendState というソフトを使う。
SetSuspendState - Vector
Sleep/Standby
> SetSuspendState
  or
> SetSuspendState 0

Hibernate
> SetSuspendState 1

非常に簡易なソフト。
サスペンドか、ハイバネートするだけ。
秒数の指定とかもできないけど、そのへんはバッチなりなんなりで対応。

ソースを見ると下のように ForceCritical の値が TRUE なので注意が必要かも。

SetSuspendState( 0, 1, 0 );  // Sleep
SetSuspendState( 1, 1, 0 );  // Hibernate


2. Ruby で SetSuspendState を実行。
コマンドラインからWindows PCを停止(スリープなど)する方法 | TipsZone


3. お好きな言語でAPIを叩く。


-------------------------
なんで shutdown.exe にサスペンド、ハイバネートのオプションないんでしょうね?


参考リンク

Windows の Rundll と Rundll32 インターフェイス
SetSuspendState function
SetSuspendState function (Windows)
Application.SetSuspendState メソッド (System.Windows.Forms)

プログラミングTips : SetSystemPowerState() の罠
コマンドラインからWindows PCを停止(スリープなど)する方法 | TipsZone
SetSuspendState - Vector