HHeLiBeXの日記 正道編

日々の記憶の記録とメモ‥

crontabで実行時刻を適当に割り振るときの一案

どちらかというと、単なるネタ記事(何)。

cronで実行する処理で、以下の要件を満たすような場合に自分が使っている実行時刻の決め方の一つ。

  • 1日1回実行されればよい
  • 似たような処理が複数あるが、随時増えていく
    • (同じコマンドのパラメータ違いを別々にcrontabのエントリにするとか)
  • 実行時刻は問わないので、負荷分散のために適当にばらけさせたい

ただ単に「時」「分」を整然と並べても面白くないので、以下のように生成している。

  • 「時」は0から23まで順番に
  • 「分」は素数を順に使用(0から59までの間に17個ある)

せっかくなので(謎)、それを生成するPHPプログラムを書いてみた。

<?php
$h = range(0, 23, 1);
$m = array();
// 素数列を生成
for($p = 1; ($p = gmp_intval(gmp_nextprime($p))) < 60;) {
    $m[] = $p;
}

$mi = 0;
$hi = 0;
do {
    printf("%02d %02d *  *  *\n", $m[$mi], $h[$hi]);
    $mi = ($mi + 1) % count($m);
    $hi = ($hi + 1) % count($h);
} while ($mi != 0 || $hi != 0);

使用している関数については、以下を参考に。

実行結果を整形して表にしてみると、以下のような感じ。(表示の都合上、crontabに書く「日」「月」「曜日」は省略)

1周目 2周目 3周目 4周目 5周目 6周目 7周目 8周目 9周目 10周目
00時台 02 00 19 00 47 00 11 00 37 00 03 00 23 00 53 00 13 00 41 00
01時台 03 01 23 01 53 01 13 01 41 01 05 01 29 01 59 01 17 01 43 01
02時台 05 02 29 02 59 02 17 02 43 02 07 02 31 02 02 02 19 02 47 02
03時台 07 03 31 03 02 03 19 03 47 03 11 03 37 03 03 03 23 03 53 03
04時台 11 04 37 04 03 04 23 04 53 04 13 04 41 04 05 04 29 04 59 04
05時台 13 05 41 05 05 05 29 05 59 05 17 05 43 05 07 05 31 05 02 05
06時台 17 06 43 06 07 06 31 06 02 06 19 06 47 06 11 06 37 06 03 06
07時台 19 07 47 07 11 07 37 07 03 07 23 07 53 07 13 07 41 07 05 07
08時台 23 08 53 08 13 08 41 08 05 08 29 08 59 08 17 08 43 08 07 08
09時台 29 09 59 09 17 09 43 09 07 09 31 09 02 09 19 09 47 09 11 09
10時台 31 10 02 10 19 10 47 10 11 10 37 10 03 10 23 10 53 10 13 10
11時台 37 11 03 11 23 11 53 11 13 11 41 11 05 11 29 11 59 11 17 11
12時台 41 12 05 12 29 12 59 12 17 12 43 12 07 12 31 12 02 12 19 12
13時台 43 13 07 13 31 13 02 13 19 13 47 13 11 13 37 13 03 13 23 13
14時台 47 14 11 14 37 14 03 14 23 14 53 14 13 14 41 14 05 14 29 14
15時台 53 15 13 15 41 15 05 15 29 15 59 15 17 15 43 15 07 15 31 15
16時台 59 16 17 16 43 16 07 16 31 16 02 16 19 16 47 16 11 16 37 16
17時台 02 17 19 17 47 17 11 17 37 17 03 17 23 17 53 17 13 17 41 17
18時台 03 18 23 18 53 18 13 18 41 18 05 18 29 18 59 18 17 18 43 18
19時台 05 19 29 19 59 19 17 19 43 19 07 19 31 19 02 19 19 19 47 19
20時台 07 20 31 20 02 20 19 20 47 20 11 20 37 20 03 20 23 20 53 20
21時台 11 21 37 21 03 21 23 21 53 21 13 21 41 21 05 21 29 21 59 21
22時台 13 22 41 22 05 22 29 22 59 22 17 22 43 22 07 22 31 22 02 22
23時台 17 23 43 23 07 23 31 23 02 23 19 23 47 23 11 23 37 23 03 23
11周目 12周目 13周目 14周目 15周目 16周目 17周目
00時台 05 00 29 00 59 00 17 00 43 00 07 00 31 00
01時台 07 01 31 01 02 01 19 01 47 01 11 01 37 01
02時台 11 02 37 02 03 02 23 02 53 02 13 02 41 02
03時台 13 03 41 03 05 03 29 03 59 03 17 03 43 03
04時台 17 04 43 04 07 04 31 04 02 04 19 04 47 04
05時台 19 05 47 05 11 05 37 05 03 05 23 05 53 05
06時台 23 06 53 06 13 06 41 06 05 06 29 06 59 06
07時台 29 07 59 07 17 07 43 07 07 07 31 07 02 07
08時台 31 08 02 08 19 08 47 08 11 08 37 08 03 08
09時台 37 09 03 09 23 09 53 09 13 09 41 09 05 09
10時台 41 10 05 10 29 10 59 10 17 10 43 10 07 10
11時台 43 11 07 11 31 11 02 11 19 11 47 11 11 11
12時台 47 12 11 12 37 12 03 12 23 12 53 12 13 12
13時台 53 13 13 13 41 13 05 13 29 13 59 13 17 13
14時台 59 14 17 14 43 14 07 14 31 14 02 14 19 14
15時台 02 15 19 15 47 15 11 15 37 15 03 15 23 15
16時台 03 16 23 16 53 16 13 16 41 16 05 16 29 16
17時台 05 17 29 17 59 17 17 17 43 17 07 17 31 17
18時台 07 18 31 18 02 18 19 18 47 18 11 18 37 18
19時台 11 19 37 19 03 19 23 19 53 19 13 19 41 19
20時台 13 20 41 20 05 20 29 20 59 20 17 20 43 20
21時台 17 21 43 21 07 21 31 21 02 21 19 21 47 21
22時台 19 22 47 22 11 22 37 22 03 22 23 22 53 22
23時台 23 23 53 23 13 23 41 23 05 23 29 23 59 23

全部で408通りできる。
使用される素数の個数である「17」自体が素数なので、割とばらけているように見える(ぇ。

参照はあいまい

いわゆるJavaの初心者がどつぼにはまりがちな、「参照はあいまい」と言われて戸惑うケース。

(NGコード)

package t2014_1008_01;

import java.util.*;
import java.sql.*;

public class Main {

    public static void main(String[] args) {
        Date now = new Date();
        System.out.println(now);
        
        // なにかDBへアクセスする処理をこの後に書く想定・・
    }

}

これをコンパイルすると、次のようなエラーメッセージが出力されます。

  • 出力言語(環境)が日本語の場合
$ LANG=ja_JP.UTF-8 javac -d classes src/t2014_1008_01/Main.java 
src/t2014_1008_01/Main.java:9: Date の参照はあいまいです。java.sql の クラス java.sql.Date と java.util の クラス java.util.Date が両方適合します。
                Date now = new Date();
                ^
src/t2014_1008_01/Main.java:9: Date の参照はあいまいです。java.sql の クラス java.sql.Date と java.util の クラス java.util.Date が両方適合します。
                Date now = new Date();
                               ^
エラー 2 個
  • 出力言語(環境)が英語の場合
$ LANG=C javac -encoding UTF-8 -d classes src/t2014_1008_01/Main.java 
src/t2014_1008_01/Main.java:9: reference to Date is ambiguous, both class java.sql.Date in java.sql and class java.util.Date in java.util match
                Date now = new Date();
                ^
src/t2014_1008_01/Main.java:9: reference to Date is ambiguous, both class java.sql.Date in java.sql and class java.util.Date in java.util match
                Date now = new Date();
                               ^
2 errors

(Java6で試していますが、それ以前のバージョンだともっとメッセージが不親切だったりするかもしないかも・・)
(CentOS 6.5で試していますが、WindowsでもMacOSでも理屈は同じ。)

で、仮に、「java.util.Date」を使うことを想定しているとして、ネットでちゃちゃっと調べて、「あぁ、『java.util.』をつければいいんだな」と、以下のように修正します。

(NGコード)

package t2014_1008_01;

import java.util.*;
import java.sql.*;

public class Main {

    public static void main(String[] args) {
        java.util.Date now = new Date();
        System.out.println(now);
        
        // なにかDBへアクセスする処理をこの後に書く想定・・
    }

}

気づく人はすぐに気づくでしょうが、これだけでは解決しません。
「new Date()」の方の型名がまだ「あいまい」ですから・・
(コンパイラは、好意的な解釈はせず、あくまで機械的に解釈しますから・・)

(OKコード)

package t2014_1008_01;

import java.util.*;
import java.sql.*;

public class Main {

    public static void main(String[] args) {
        java.util.Date now = new java.util.Date();
        System.out.println(now);
        
        // なにかDBへアクセスする処理をこの後に書く想定・・
    }

}

このようにしないといけません。


「わぁ、エラーメッセージだ、ググってみよう」と一つの流れのように機械的に対処しようとすると、探し当てたサンプルが自分のはまっている問題にピッタリ合致しないものだった場合に、このような罠にはまります。

辛くても、最初のコードをコンパイルしたときのエラーメッセージを「しっかり」見てみると、「2個のエラーが出ている」ことが分かります。

  • 一つは、変数宣言で型を指定するときの「Date」があいまい。
  • もう一つは、オブジェクトをnewする際に指定する「Date」があいまい。


まぁ、そもそも、import文には、「ワイルドカード指定よりも個別指定の方が優先される」という仕様があるので、「java.util.Date」と「java.sql.Date」の両方を使う必要性がない限り、以下のように書けば十分なんですが・・

(OKコード)

package t2014_1008_01;

import java.util.Date; // 個別指定
import java.util.*;    // ワイルドカード指定
import java.sql.*;     // ワイルドカード指定

public class Main {

    public static void main(String[] args) {
        Date now = new Date();
        System.out.println(now);
        
        // なにかDBへアクセスする処理をこの後に書く想定・・
    }

}

そもそも、IDEも普及している現代では、私はワイルドカード指定「非」推奨派ですが・・
・・と言いながらも、時々めんどくさくなって、テキストエディタでコードを書くときがある奴(ぇ

switch文の罠

PHPで(知らずに)以下のようなコードを書いていてはまった。

<?php

$sum = 0;
foreach (range(1, 5) as $i) {
    $val = $i;

    printf("%3d\n", $val);
    switch ($val) {
    case 1:
        printf("%5d: %s\n", $val, "One");
        break;
    case 2:
        printf("%5d: %s\n", $val, "Two");
        break;
    case 3:
        printf("%5d: %s\n", $val, "Three");
        continue;
    case 4:
        printf("%5d: %s\n", $val, "Four");
    case 5:
        printf("%5d: %s\n", $val, "Five");
        break;
    }
    $sum += $val;
}
printf("sum=%d\n", $sum);

実行すると以下のような結果。

  1
    1: One
  2
    2: Two
  3
    3: Three
  4
    4: Four
    4: Five
  5
    5: Five
sum=15

‥あれ、「$val == 3」のときはスキップされるから、「sum=12」になるはず‥

ためしに色んな言語で(時には無理矢理(謎))同じことをしてみる。

Javaの場合

public class Hoge {
    public static void main(String[] args) {
        int sum = 0;
        for (int i = 0; i < 5; ++i) {
            int val = i + 1;

            System.out.printf("%3d%n", val);
            switch (val) {
            case 1:
                System.out.printf("%5d: %s%n", val, "One");
                break;
            case 2:
                System.out.printf("%5d: %s%n", val, "Two");
                break;
            case 3:
                System.out.printf("%5d: %s%n", val, "Three");
                continue;
            case 4:
                System.out.printf("%5d: %s%n", val, "Four");
            case 5:
                System.out.printf("%5d: %s%n", val, "Five");
                break;
            }
            sum += (val);
        }
        System.out.printf("sum=%d%n", sum);
    }
}

実行結果。

  1
    1: One
  2
    2: Two
  3
    3: Three
  4
    4: Four
    4: Five
  5
    5: Five
sum=12

うん、「sum=12」になる。

C言語の場合

#include <stdio.h>

int main(int argc, char* argv[]) {
    int i;
    int sum = 0;
    int val;

    for (i = 0; i < 5; ++i) {
        val = i + 1;

        printf("%3d\n", val);
        switch (val) {
        case 1:
            printf("%5d: %s\n", val, "One");
            break;
        case 2:
            printf("%5d: %s\n", val, "Two");
            break;
        case 3:
            printf("%5d: %s\n", val, "Three");
            continue;
        case 4:
            printf("%5d: %s\n", val, "Four");
        case 5:
            printf("%5d: %s\n", val, "Five");
            break;
        }
        sum += val;
    }
    printf("sum=%d\n", sum);
}

実行結果。

  1
    1: One
  2
    2: Two
  3
    3: Three
  4
    4: Four
    4: Five
  5
    5: Five
sum=12

うん、「sum=12」。

bash

#! /bin/bash

sum=0
for ((i = 0; i < 5; ++i)); do
    val=$((i + 1))

    printf "%3d\n" $val
    case $val in
        1)
            printf "%5d: %s\n" $val "One"
            ;;
        2)
            printf "%5d: %s\n" $val "Two"
            ;;
        3)
            printf "%5d: %s\n" $val "Three"
            continue;
            ;;
        4|5)
            case $val in
                4)
                    printf "%5d: %s\n" $val "Four"
                    ;;
            esac
            printf "%5d: %s\n" $val "Five"
            ;;
    esac
    sum=$((sum + val))
done
printf "sum=%d\n" $sum

break文に当たる「;;」を省略できないので、fall throughの部分は無理矢理だが。
実行結果。

  1
    1: One
  2
    2: Two
  3
    3: Three
  4
    4: Four
    4: Five
  5
    5: Five
sum=12

うん、「sum=12」

Perl

Perlは書けないので、以下の辺りを参考に。

use Switch;

my $i;
my $sum = 0;

for ($i = 0; $i < 5; ++$i) {
    my $val = $i + 1;

    printf("%3d\n", $val);
    switch ($val) {
        case 1 {
            printf("%5d: %s\n", $val, "One");
        }
        case 2 {
            printf("%5d: %s\n", $val, "Two");
        }
        case 3 {
            printf("%5d: %s\n", $val, "Three");
            next;
        }
        case [4, 5] {
            switch ($val) {
                case 4 {
                    printf("%5d: %s\n", $val, "Four");
                }
            }
            printf("%5d: %s\n", $val, "Five");
        }
    }
    $sum += $val;
}
printf("sum=%d\n", $sum);

同じくfall throughの部分は無理矢理。
実行結果。

  1
    1: One
  2
    2: Two
  3
    3: Three
  4
    4: Four
    4: Five
  5
    5: Five
sum=15

‥あれ、「sum=15」だ‥
下記で追調査。

Ruby

Rubyも書けないので、以下の辺りを参考に。

ちなみに、脱線するが、この辺りが面白かった。

sum=0
for i in 1..5 do
    val = i;

    printf("%3d\n", val);
    case val
    when 1
        printf("%5d: %s\n", val, "One");
    when 2
        printf("%5d: %s\n", val, "Two");
    when 3
        printf("%5d: %s\n", val, "Three");
        next;
    when 4, 5
        case val
        when 4
            printf("%5d: %s\n", val, "Four");
        end
        printf("%5d: %s\n", val, "Five");
    end
    sum += val;
end
printf("sum=%d\n", sum);

同じくfall throughの部分は無理矢理。
実行結果。

  1
    1: One
  2
    2: Two
  3
    3: Three
  4
    4: Four
    4: Five
  5
    5: Five
sum=12

うん、「sum=12」。

一旦まとめると‥

言語 switch(case)文の中でのループスキップ
PHP  ×(※1)
Java  ○
C言語  ○
bash  ○
Perl  ×(※2)
Ruby  ○

と、こんな感じになった。
‥って書いちゃうと誤解を招くので、さっさと追調査。

(※1)PHPの場合の仕様

実は、マニュアルの中にこんなことが書いてあった。


注意: 他の言語とは違って、 continue命令は switchにも適用され、breakと同じ動作をします。 ループの内部でswitchを使用しており、 外側のループの処理を続行させたい場合には、continue 2 を使用してください。

‥おぉ!
ということで、以下のように書き直してみる。

<?php

$sum = 0;
foreach (range(1, 5) as $i) {
    $val = $i;

    printf("%3d\n", $val);
    switch ($val) {
    case 1:
        printf("%5d: %s\n", $val, "One");
        break;
    case 2:
        printf("%5d: %s\n", $val, "Two");
        break;
    case 3:
        printf("%5d: %s\n", $val, "Three");
        continue 2;
    case 4:
        printf("%5d: %s\n", $val, "Four");
    case 5:
        printf("%5d: %s\n", $val, "Five");
        break;
    }
    $sum += $val;
}
printf("sum=%d\n", $sum);

実行結果。

  1
    1: One
  2
    2: Two
  3
    3: Three
  4
    4: Four
    4: Five
  5
    5: Five
sum=12

うん、「sum=12」。

この構文、もちろん単なる多重ループのときにも有効で、変なラベル(何)をつける必要がないというメリットがある。一方で、ぱっと見ではどこに飛ぶのかが分からず、ループの数を数え間違えるとひどい事になるというデメリットがある。

(※2)Perlの場合の仕様

(PHPと同じように書いてみたときのエラーメッセージからたどり着いたのは内緒(謎))

Perlでは、ラベルによって、どのループに対する「next」なのかを指定できる。

そもそも、PerlのSwitchモジュールにおける「next」はfall throughのためのものらしい。

つまり、先のPerlプログラム中の「case 3」よりも下に、$valが3のときにマッチするcase句があったら違う結果を引き起こしていたというわけだ。怖い怖い‥

というわけで、上記を踏まえて書き直したのが以下。

use Switch;

my $i;
my $sum = 0;

loop:
for ($i = 0; $i < 5; ++$i) {
    my $val = $i + 1;

    printf("%3d\n", $val);
    switch ($val) {
        case 1 {
            printf("%5d: %s\n", $val, "One");
        }
        case 2 {
            printf("%5d: %s\n", $val, "Two");
        }
        case 3 {
            printf("%5d: %s\n", $val, "Three");
            next loop;
        }
        case 4 {
            printf("%5d: %s\n", $val, "Four");
            next;
        }
        case [4, 5] {
            printf("%5d: %s\n", $val, "Five");
        }
    }
    $sum += $val;
}
printf("sum=%d\n", $sum);

Switchモジュールのfall throughの仕様をちゃんと使ってみた。
また、この「ラベル」の概念はJavaでも同じですね。(もっと言えば、悪しきモノとして封印されているC言語のgotoとか(略))
で、実行結果。

  1
    1: One
  2
    2: Two
  3
    3: Three
  4
    4: Four
    4: Five
  5
    5: Five
sum=12

うん、「sum=12」。

で‥

自分の場合、JavaC言語bashスクリプトに親しんでからPHPを使うようになった派なのでこのような罠にどっぷりはまったわけだが、「構文が似ているからと言って、同じ動作をするとは限らない」という教訓になる。
上層の人(誰)の「君はこのプログラム言語できるよね?こっちの言語も似たようなものだから余裕でしょ?ちょっとこっちの案件手伝ってくれる?あ、言語の習得は案件を進めながら合間にやってね」という台詞に、全力で立ち向かえることと思う。「似ているからこそ危険なんだよ!」と。

関数やメソッドの引数のタイプヒント指定の罠

久しぶりにネタとしてメモしておきたい事象にぶつかったのでメモ。

「罠」とは言っても、熟練のPHPerにとっては当たり前のことなんだろうけど‥

PHP(PHP 5環境)で以下のようなコードを書いていた。

<?php

function hoge_int(int $val) {
    var_dump(__FUNCTION__, $val);
}

hoge_int(10);

さて、これを実行してみると‥

PHP Catchable fatal error:  Argument 1 passed to hoge_int() must be an instance of int, integer given, called in C:\temp\hoge.php on line 7 and defined in C:\temp\hoge.php on line 3

一瞬、何のことだか意味が分かりません。
渡しているのはint(integer)値だし、タイプヒント指定と食い違っているようには見えない。
そこで、エラーメッセージを元に検索してみると、以下のサイトを見つけた。

types - Really PHP? "Argument 1 passed to my_function() must be an instance of string, string given" - Stack Overflow

まさかと思いながら、以下のコードを試してみる。

<?php

function hoge_int(int $val) {
    var_dump(__FUNCTION__, $val);
}

final class int {
    private $_val;
    public function __construct($val) {
        if (is_int($val)) {
            $this->_val = $val;
        } else {
            trigger_error('No, you fool!', E_USER_NOTICE);
        }
    }
    public function __toString() {
        return (string)$this->_val;
    }
}

hoge_int(new int(10));

すると、以下の出力を得ることができる。関数が正常に呼ばれた証拠。

string(8) "hoge_int"
object(int)#1 (1) {
  ["_val":"int":private]=>
  int(10)
}

さて、先のエラーメッセージをよーく読んでみると、こんな風に書いてある。

must be an instance of int, integer given

「instance of int」を渡すべきところで「integer」を渡している、と。

そこで、さらに以下のようなことをしてみる。

<?php

function hoge_int(int $val) {
    var_dump(__FUNCTION__, $val);
}

final class int {
    private $_val;
    public function __construct($val) {
        if (is_int($val)) {
            $this->_val = $val;
        } else {
            trigger_error('No, you fool!', E_USER_NOTICE);
        }
    }
    public function __toString() {
        return (string)$this->_val;
    }
}

final class integer {
    private $_val;
    public function __construct($val) {
        if (is_integer($val)) {
            $this->_val = $val;
        } else {
            trigger_error('No, you fool!', E_USER_NOTICE);
        }
    }
    public function __toString() {
        return (string)$this->_val;
    }
}

hoge_int(new integer(10));

すると、こんなエラーメッセージが。

PHP Catchable fatal error:  Argument 1 passed to hoge_int() must be an instance of int, instance of integer given, called in C:\temp\hoge.php on line 35 and defined in C:\temp\hoge.php on line 3

今度は、「instance of int」を渡すべきところで「instance of integer」を渡している、と書かれている。
おぉ、いわゆるオブジェクト型でないといけないということじゃないか‥


あんまり話を引っ張っても別に面白くないので、上記サイトからもリンクされているPHPのマニュアル「Type Hinting/タイプヒンティング」を見てみる。

よく読めばちゃんと書いてあるじゃないか‥

Type hints can not be used with scalar types such as int or string.
タイプヒントは int や string といったスカラー型には使えません。 

マニュアルによれば、タイプヒントとして指定可能なのは以下のものらしい。

  • クラス名
  • インタフェース名
  • 配列 (PHP 5.1以降)
  • callable (PHP 5.4以降)

また、タイプヒントが付いたパラメータにはNULLを渡すことはできないが、パラメータのデフォルト値として「= null」と指定しておけばnullを渡すことも可能らしい。

じゃあ、スカラー型の指定を強制したい場合はどうするかというと、冒頭に挙げたサイトにも書いてあるが、

<?php

function hoge_int($val) {
    if (!is_int($val)) {
        trigger_error('No, you fool!', E_USER_ERROR);
    }
    var_dump(__FUNCTION__, $val);
}

hoge_int(10);

というコートを書くしかないらしい。


なんか、Autoboxing/Unboxingが実装される前のJavaでプリミティブ型の値をラッパークラスを使ってオブジェクトにしてからListに放り込むのめんどくさい‥なんてことをしていたのを思い出した‥

よかった探しリース

←左手よかった探しリース右手→
今年も「よかった探しリース」に参加させていただきます。

今年は後半から復活の兆しが見え、自分にできることをマイペースでやっていこうと思い始めている今日この頃です。

  • 家族が大過なく過ごせたこと
    • 私自身もだいぶ調子が戻ってきました
  • 古巣に近いところに転職したが、皆温かく迎えてくれたこと
    • と同時に、前職で大変なこともあったけど、無駄な経験ではなかったこと
  • 艦これ」楽しい
    • でも、リアル戦争はダメ!絶対!
  • ドラマ「ドクターX」(テレビ朝日系)の第二期も面白い(現在進行形(謎))
  • アニメ「ログ・ホライズン」(NHK)が意外と面白い
    • エンディング曲を歌う歌手「Yun*chi」の今後に期待
  • HTML5/CSS3を勉強し始めて、やってみると意外に面白いなと気づいたこと
    • さっそく実践投入中
  • 映画「風立ちぬ」を久々の映画館で鑑賞‥映画を観て泣いたのも久々な気がする
    • やっぱり、リアル戦争は悲しみしか生まないな‥

牛の歩み(何)で少しずつ追加していきます。

音楽ファイルや動画ファイルの取得にも認証を導入したときにぶつかった問題

もう既に2年近く前、Android 2.3がまだ新しいと言われていた頃の話だが、当時は相当苦しんだので、検証記録として残してみる。

はじめに

とあるWebアプリケーションの開発を担当している人から、「Androidの実機だと音が再生されない。どうにも原因が分からないので助けて欲しい」という相談を受け、その辺の知識もほとんどないままにヘルプすることになった。
話を聞いてみると、以下の状況ということが分かった。

  • そのWebアプリケーションは、認証処理時に発行したトークンをクッキーに保存し、それをチェックすることで認証済みかどうかを確認する形のもの。
  • 開発時はPCのブラウザにUAを偽装するような拡張機能を入れて動作を見ながら開発している。
  • いざ実機で確認してみると‥
    • iPhoneでアクセスすると、期待通りに音楽ファイルが再生されて音が出る。
    • Android端末でアクセスすると、音が全く出ない。
      • ちなみに、確かメディア再生のHTMLタグがサポートされたのがAndroid 2.3からで、当時私が持っていた「Galaxy S」がAndroid 2.2からAndroid 2.3へのアップデートが提供された頃だった。

下準備

せっかくだからということで、今回、audioタグとvideoタグ、比較としてimgタグを使った検証をしようとテストプログラムを作っていたら、動画に関しては生半可な対応では再生されなかったので(どうやらHTTP 206(Partial Content)を使用するらしい)、Apache httpdのxsendfileモジュールを使用した。
導入に関しては、ここでの説明は手抜きするが、以下のサイトを参考にした。

ざっくり書くと、以下のような感じ。(CentOS 6.4での実行例)

# yum groupinstall "Development Tools"
# yum install httpd httpd-devel
# curl -o mod_xsendfile.c https://tn123.org/mod_xsendfile/mod_xsendfile.c
# apxs -cia mod_xsendfile-0.12/mod_xsendfile.c 
# vi /etc/httpd/conf.d/xsendfile.conf
<IfModule mod_xsendfile.c>
    XsendFile on
    XsendFilePath /var/www/html
</IfModule>
# service httpd configtest
# service httpd restart
  • ※「yum groupinstall "Development Tools"」は代わりに「yum install gcc」だけやればおそらく大丈夫だと思うが、何が足りないか検証するのも面倒なので(待て)。まぁ今回の主目的ではないからということで。
  • ※今回はXsendFilePathを公開ディレクトリにしているが、これは検証用なので、本来はDocumentRoot以外の直接アクセス不可能なディレクトリにすべきです、念のため。

認証無しのケースでの確認

「そもそも再生されない」っていうことだと困るので、まずは以下のプログラムで試す。

<?php

$user = '-';

$time = time();

$msg = array();
$msg[] = "HTML5Test[{$_SERVER['REQUEST_METHOD']}]";
$msg[] = "\"{$user}\"";
if (isset($_GET['file'], $_GET['type'])) {
    $size = filesize($_GET['file']);
    header("Content-Length: {$size}");
    header("Content-Type: {$_GET['type']}");
    header("X-Sendfile: {$_GET['file']}");

    $msg[] = "\"file={$_GET['file']}&type={$_GET['type']}\"";
} else {
    $msg[] = "\"file=view&type=text/html\"";
?>
<div>
    <img src="?file=image1.png&amp;type=image/png&amp;time=<?php echo $time;?>" />
</div>
<div>
    <audio src="?file=audio1.mp3&amp;type=audio/mpeg&amp;time=<?php echo $time;?>"
            autoplay="autoplay"
            controls="controls">
        audio not supported
    </audio>
</div>
<div>
    <video src="?file=video1.mp4&amp;type=video/mp4&amp;time=<?php echo $time;?>"
            autoplay="autoplay"
            controls="controls"
            width="320" height="240">
        video not supported
    </video>
</div>
<?php
}

$msg[] = "\"{$_SERVER['HTTP_USER_AGENT']}\"";
$msg[] = "\"{$_SERVER['REQUEST_URI']}\"";
trigger_error(implode(' ', $msg), E_USER_NOTICE);
?>

timeパラメータは、一応のキャッシュ対策。
これで、以下のケースを試してみる。

実際にアクセスすると、Apache httpdのエラーログに以下のようなログが記録される。(PCのGoogle Chromeでの出力例)

[Fri Dec 06 14:35:56 2013] [error] [client 192.168.0.121] PHP Notice:  HTML5Test[GET] "-" "file=view&type=text/html" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36" "/test-noauth.php" in /var/www/html/test-noauth.php on line 43, referer: http://192.168.201.200/
[Fri Dec 06 14:35:56 2013] [error] [client 192.168.0.121] PHP Notice:  HTML5Test[GET] "-" "file=image1.png&type=image/png" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36" "/test-noauth.php?file=image1.png&type=image/png&time=1386308156" in /var/www/html/test-noauth.php on line 43, referer: http://192.168.201.200/test-noauth.php
[Fri Dec 06 14:35:56 2013] [error] [client 192.168.0.121] PHP Notice:  HTML5Test[GET] "-" "file=audio1.mp3&type=audio/mpeg" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36" "/test-noauth.php?file=audio1.mp3&type=audio/mpeg&time=1386308156" in /var/www/html/test-noauth.php on line 43, referer: http://192.168.201.200/test-noauth.php
[Fri Dec 06 14:35:56 2013] [error] [client 192.168.0.121] PHP Notice:  HTML5Test[GET] "-" "file=video1.mp4&type=video/mp4" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36" "/test-noauth.php?file=video1.mp4&type=video/mp4&time=1386308156" in /var/www/html/test-noauth.php on line 43, referer: http://192.168.201.200/test-noauth.php
[Fri Dec 06 14:35:56 2013] [error] [client 192.168.0.121] PHP Notice:  HTML5Test[GET] "-" "file=video1.mp4&type=video/mp4" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36" "/test-noauth.php?file=video1.mp4&type=video/mp4&time=1386308156" in /var/www/html/test-noauth.php on line 43, referer: http://192.168.201.200/test-noauth.php
[Fri Dec 06 14:35:56 2013] [error] [client 192.168.0.121] PHP Notice:  HTML5Test[GET] "-" "file=video1.mp4&type=video/mp4" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36" "/test-noauth.php?file=video1.mp4&type=video/mp4&time=1386308156" in /var/www/html/test-noauth.php on line 43, referer: http://192.168.201.200/test-noauth.php

ちなみに、書いてあるIPアドレスはすべてプライベートアドレスなので、アタックしようとしても無駄よん、念のため(謎)。

オレオレ認証処理を追加して検証

今回、認証機構を作るのが目的ではないので、ユーザIDを入力してPOSTすると、それをセッションに保存しておき、そのデータの有無で認証済みかどうかを判定するという手抜きな仕組みで代用する。(PHPのセッションはクッキーに保存されたIDで識別する設定になっていて、今回の要件には合っているので)

<?php

session_start();
if (isset($_POST['logout'])) {
        unset($_SESSION['authorized']);
}
if (isset($_POST['username'])) {
        $_SESSION['authorized'] = $_POST['username'];
}
$user = isset($_SESSION['authorized']) ? $_SESSION['authorized'] : '-';

$time = time();

$msg = array();
$msg[] = "HTML5Test[{$_SERVER['REQUEST_METHOD']}]";
$msg[] = "\"{$user}\"";
if (isset($_SESSION['authorized'])) {
        if (isset($_GET['file'], $_GET['type'])) {
                $size = filesize($_GET['file']);
                header("Content-Length: {$size}");
                header("Content-Type: {$_GET['type']}");
                header("X-Sendfile: {$_GET['file']}");

                $msg[] = "\"file={$_GET['file']}&type={$_GET['type']}\"";
        } else {
                $msg[] = "\"file=view&type=text/html\"";
?>
<div>
        <img src="?file=image1.png&amp;type=image/png&amp;time=<?php echo $time;?>" />
</div>
<div>
        <audio src="?file=audio1.mp3&amp;type=audio/mpeg&amp;time=<?php echo $time;?>"
                        autoplay="autoplay"
                        controls="controls">
                audio not supported
        </audio>
</div>
<div>
        <video src="?file=video1.mp4&amp;type=video/mp4&amp;time=<?php echo $time;?>"
                        autoplay="autoplay"
                        controls="controls"
                        width="320" height="240">
                video not supported
        </video>
</div>
<hr />
<form action="" method="post">
<input type="submit" name="logout" value="Logout" />
</form>
<?php
        }

} else {
        $msg[] = "\"file=login&type=text/html\"";
?>
<form action="" method="post">
<input type="text" name="username" /><br />
<input type="submit" value="Authz" />
</form>
<?php
}

$msg[] = "\"{$_SERVER['HTTP_USER_AGENT']}\"";
$msg[] = "\"{$_SERVER['REQUEST_URI']}\"";
trigger_error(implode(' ', $msg), E_USER_NOTICE);
?>

認証無しのケースから変わったのは以下。

  • 3〜10行目でオレオレ認証情報の取得などをしている。一応、ログアウト機能も実装。
  • 17行目で認証済みかどうかの条件判定
  • 46〜59行目でログアウトのためのフォームと認証のためのフォームを、それぞれの場合分けに従って表示。

これで先ほどと同じケースを試してみる。(今度は、すべてのケースでのログを、見やすいように余分な情報を除去して載せてみる)

HTML5Test[GET] "-" "file=login&type=text/html" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36" "/test-auth.php" in /var/www/html/test-auth.php on line 65
HTML5Test[POST] "hhelibex" "file=view&type=text/html" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36" "/test-auth.php" in /var/www/html/test-auth.php on line 65, referer: http://192.168.201.200/test-auth.php
HTML5Test[GET] "hhelibex" "file=image1.png&type=image/png" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36" "/test-auth.php?file=image1.png&type=image/png&time=1386308835" in /var/www/html/test-auth.php on line 65, referer: http://192.168.201.200/test-auth.php
HTML5Test[GET] "hhelibex" "file=audio1.mp3&type=audio/mpeg" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36" "/test-auth.php?file=audio1.mp3&type=audio/mpeg&time=1386308835" in /var/www/html/test-auth.php on line 65, referer: http://192.168.201.200/test-auth.php
HTML5Test[GET] "hhelibex" "file=video1.mp4&type=video/mp4" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36" "/test-auth.php?file=video1.mp4&type=video/mp4&time=1386308835" in /var/www/html/test-auth.php on line 65, referer: http://192.168.201.200/test-auth.php
HTML5Test[GET] "hhelibex" "file=video1.mp4&type=video/mp4" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36" "/test-auth.php?file=video1.mp4&type=video/mp4&time=1386308835" in /var/www/html/test-auth.php on line 65, referer: http://192.168.201.200/test-auth.php
HTML5Test[GET] "hhelibex" "file=video1.mp4&type=video/mp4" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36" "/test-auth.php?file=video1.mp4&type=video/mp4&time=1386308835" in /var/www/html/test-auth.php on line 65, referer: http://192.168.201.200/test-auth.php
HTML5Test[GET] "-" "file=login&type=text/html" "Mozilla/5.0 (Linux; U; Android 2.3.6; ja-jp; SC-02B Build/GINGERBREAD) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1" "/test-auth.php" in /var/www/html/test-auth.php on line 65, referer: http://192.168.201.200/
HTML5Test[POST] "hhelibex-2.3" "file=view&type=text/html" "Mozilla/5.0 (Linux; U; Android 2.3.6; ja-jp; SC-02B Build/GINGERBREAD) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1" "/test-auth.php" in /var/www/html/test-auth.php on line 65, referer: http://192.168.201.200/test-auth.php
HTML5Test[GET] "hhelibex-2.3" "file=image1.png&type=image/png" "Mozilla/5.0 (Linux; U; Android 2.3.6; ja-jp; SC-02B Build/GINGERBREAD) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1" "/test-auth.php?file=image1.png&type=image/png&time=1386308864" in /var/www/html/test-auth.php on line 65, referer: http://192.168.201.200/test-auth.php
HTML5Test[GET] "-" "file=login&type=text/html" "stagefright/1.1 (Linux;Android 2.3.6)" "/test-auth.php?file=audio1.mp3&type=audio/mpeg&time=1386308864" in /var/www/html/test-auth.php on line 65
HTML5Test[GET] "hhelibex-2.3" "file=video1.mp4&type=video/mp4" "stagefright/1.1 (Linux;Android 2.3.6)" "/test-auth.php?file=video1.mp4&type=video/mp4&time=1386308864" in /var/www/html/test-auth.php on line 65
HTML5Test[GET] "hhelibex-2.3" "file=video1.mp4&type=video/mp4" "stagefright/1.1 (Linux;Android 2.3.6)" "/test-auth.php?file=video1.mp4&type=video/mp4&time=1386308864" in /var/www/html/test-auth.php on line 65
HTML5Test[GET] "hhelibex-2.3" "file=video1.mp4&type=video/mp4" "stagefright/1.1 (Linux;Android 2.3.6)" "/test-auth.php?file=video1.mp4&type=video/mp4&time=1386308864" in /var/www/html/test-auth.php on line 65
  • Android 4.2(Galaxy S4)の標準ブラウザ
HTML5Test[GET] "-" "file=login&type=text/html" "Mozilla/5.0 (Linux; Android 4.2.2; ja-jp; SC-04E Build/JDQ39) AppleWebKit/535.19 (KHTML, like Gecko) Version/1.0 Chrome/18.0.1025.308 Mobile Safari/535.19" "/test-auth.php" in /var/www/html/test-auth.php on line 65
HTML5Test[POST] "hhelibex-4.2" "file=view&type=text/html" "Mozilla/5.0 (Linux; Android 4.2.2; ja-jp; SC-04E Build/JDQ39) AppleWebKit/535.19 (KHTML, like Gecko) Version/1.0 Chrome/18.0.1025.308 Mobile Safari/535.19" "/test-auth.php" in /var/www/html/test-auth.php on line 65, referer: http://192.168.201.200/test-auth.php
HTML5Test[GET] "hhelibex-4.2" "file=image1.png&type=image/png" "Mozilla/5.0 (Linux; Android 4.2.2; ja-jp; SC-04E Build/JDQ39) AppleWebKit/535.19 (KHTML, like Gecko) Version/1.0 Chrome/18.0.1025.308 Mobile Safari/535.19" "/test-auth.php?file=image1.png&type=image/png&time=1386309043" in /var/www/html/test-auth.php on line 65, referer: http://192.168.201.200/test-auth.php
HTML5Test[GET] "hhelibex-4.2" "file=audio1.mp3&type=audio/mpeg" "stagefright/1.2 (Linux;Android 4.2.2)" "/test-auth.php?file=audio1.mp3&type=audio/mpeg&time=1386309043" in /var/www/html/test-auth.php on line 65
HTML5Test[GET] "hhelibex-4.2" "file=video1.mp4&type=video/mp4" "stagefright/1.2 (Linux;Android 4.2.2)" "/test-auth.php?file=video1.mp4&type=video/mp4&time=1386309043" in /var/www/html/test-auth.php on line 65
HTML5Test[GET] "hhelibex-4.2" "file=audio1.mp3&type=audio/mpeg" "stagefright/1.2 (Linux;Android 4.2.2)" "/test-auth.php?file=audio1.mp3&type=audio/mpeg&time=1386309043" in /var/www/html/test-auth.php on line 65
HTML5Test[GET] "hhelibex-4.2" "file=video1.mp4&type=video/mp4" "stagefright/1.2 (Linux;Android 4.2.2)" "/test-auth.php?file=video1.mp4&type=video/mp4&time=1386309043" in /var/www/html/test-auth.php on line 65
HTML5Test[GET] "hhelibex-4.2" "file=video1.mp4&type=video/mp4" "stagefright/1.2 (Linux;Android 4.2.2)" "/test-auth.php?file=video1.mp4&type=video/mp4&time=1386309043" in /var/www/html/test-auth.php on line 65
HTML5Test[GET] "hhelibex-4.2" "file=video1.mp4&type=video/mp4" "stagefright/1.2 (Linux;Android 4.2.2)" "/test-auth.php?file=video1.mp4&type=video/mp4&time=1386309043" in /var/www/html/test-auth.php on line 65
HTML5Test[GET] "hhelibex-4.2" "file=video1.mp4&type=video/mp4" "stagefright/1.2 (Linux;Android 4.2.2)" "/test-auth.php?file=video1.mp4&type=video/mp4&time=1386309043" in /var/www/html/test-auth.php on line 65

ログだけではちょっと分からないと思うが、以下のような動作をしている。

  • PCのGoogle Chromeでは、音声、動画ともに自動再生される。
  • Android 2.3では、音声は自動再生される(しようとする)が、動画は再生ボタンを押さないと再生されない。
  • Android 4.2では、音声、動画ともに自動再生される。
    • ただ、複数のメディアの同時再生はできないようで、音声再生中に動画の再生が始まると、音声の再生が一時停止するらしい。逆も同じ。

ちなみに、動画へのアクセスのログが多いのは、再生時に複数回に分けて分割して取得していたりするからだと思われる。

で、パッと見ただけでは分かりにくいかもしれないが、Android 2.3の音声ファイル取得のログが以下のようになっている。

HTML5Test[GET] "-" "file=login&type=text/html" "stagefright/1.1 (Linux;Android 2.3.6)" "/test-auth.php?file=audio1.mp3&type=audio/mpeg&time=1386308864" in /var/www/html/test-auth.php on line 65

まず、ユーザ名が取得できていないことから、セッション識別に必要なクッキーが送られていないと推測される。「file=login」と出力されていることからも、ベースとなるHTML文書の取得とセッションを共有できていないことが分かる。
と、それ以前に、UAが「stagefright/1.1」となっていて、HTMLファイルや画像ファイルとは異なるUAによってアクセスされていることも分かる。
Android 4.2では「stagefright/1.2」が使用され、ユーザ名もちゃんと取れていることから、Android 2.3におけるバグなのかな、と推測される(Android 3.xの端末がないのでそこの検証はできないが‥)

おわりに

今回の動作検証では、手元にあった動画ファイルが大きすぎたので、以下のサイトにある動画素材を使わせてもらいました。

margin/border/paddingの指定方法メモ

CSSのmargin/border/paddingは、四辺を一括で指定する方法から四辺を個別に指定する方法まであって、どの形式でどの辺が影響を受けるのかがしょっちゅう分からなくなるので、自分用のメモ。

一応、以下のブラウザで確認。

本当はブログ記事上で表現できればよかったのだが、はてな記法をエスケープしたりするのが面倒だったので、画面キャプチャを貼り付け(ぉ

ちなみに、使用したソースは以下のとおり。

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>margin/border/padding</title>
</head>
<body>
<style type="text/css">
.my_root {
	background-color: #888;
	margin: 0;
	border-width: 0;
	padding: 10px;
}
.my_frame {
	border-width: 2px;
	border-color: yellow;
	border-style: solid;
	display: inline-block;
}
.my_sample {
	border-style: solid;
	background-color: #fff;
}
.my_sample1_1 {
	margin:       10px;
	border-width: 20px;
	border-color: #f00;
	padding:      40px;
}
.my_sample2_1 {
	margin:       10px;
	border-width: 10px;
	border-color: #f00;
	padding:      10px;
}
.my_sample2_2 {
	margin:       10px 20px;
	border-width: 10px 20px;
	border-color: #f00 #0f0;
	padding:      10px 20px;
}
.my_sample2_3 {
	margin:       10px 20px 30px;
	border-width: 10px 20px 30px;
	border-color: #f00 #0f0 #00f;
	padding:      10px 20px 30px;
}
.my_sample2_4 {
	margin:       10px 20px 30px 40px;
	border-width: 10px 20px 30px 40px;
	border-color: #f00 #0f0 #00f #ff0;
	padding:      10px 20px 30px 40px;
}
</style>
<div class="my_root">
<div class="my_frame">
<pre class="my_sample my_sample1_1">
<外から順に
    margin
    border
    padding      >
margin:       10px;
border-width: 20px;
border-color: #f00;
padding:      40px;
</pre>
</div>

<div style="clear: both;"></div>

<div class="my_frame">
<pre class="my_sample my_sample2_1">
<四辺すべて>
margin:       10px;
border-width: 10px;
border-color: #f00;
padding:      10px;
</pre>
</div>
<div class="my_frame">
<pre class="my_sample my_sample2_2">
<上下><左右>
margin:       10px 20px;
border-width: 10px 20px;
border-color: #f00 #0f0;
padding:      10px 20px;
</pre>
</div>

<div style="clear: both;"></div>

<div class="my_frame">
<pre class="my_sample my_sample2_3">
<上><左右><下>
margin:       10px 20px 30px;
border-width: 10px 20px 30px;
border-color: #f00 #0f0 #00f;
padding:      10px 20px 30px;
</pre>
</div>
<div class="my_frame">
<pre class="my_sample my_sample2_4">
<上><右><下><左>
margin:       10px 20px 30px 40px;
border-width: 10px 20px 30px 40px;
border-color: #f00 #0f0 #00f #ff0;
padding:      10px 20px 30px 40px;
</pre>
</div>
</div>
</body>
</html>