PHP で exec 関数を使ってブラウザから呼び出した命令を実行できない時の対処法

公開日:
最終更新日:
PHP の exec 関数が実行できない

PHP を使ってボタンを実装してブラウザから押して,何らかの処理を実行しようとしてエラーを吐かれた経験がある人はかなり多いのではないかと思います.
その中でいくつか特に詰まった点を解説していきたいと思います.
主にパスが通っていないことが問題であることが多いので,パスを確認するのがいいと思います.

1. ログの出し方

まず,デバッグをするにはエラーメッセージを出さないと始まりません.
test.phpにアクセスした時にhoge 命令を実行し,そのエラーメッセージを確認したい時は以下のようにします.

exec("hoge 2>&1",$dum,$rtn); // 2>&1 で標準エラー出力も標準出力へ吐き出す
echo('<pre>'); // フォーマットを整える
var_dump($dum); // 標準出力 (標準エラー出力を含む) をブラウザに出力
echo('<pre>');
echo("return: $rtn"); // hoge 命令の返り値を "return: 返り値" の形で出力

これで,ボタンを押した後にブラウザにエラーメッセージが表示されるはずです.
基本的にこのエラーを見て,ググれば大体は解決するはずですが,少し詰まりどころを解説していきます.

2. Not Found

エラーメッセージが例えば以下のようなものだったとします.

array(1) {
  [0]=>
  string(23) "sh: 1: hoge: not found"
}
return: 127

not found の意味通り, hoge 命令が見つからないと言っています.
まず,terminal から hoge が実行されるか確認します.

$ hoge

2.1. これで実行できない場合

hoge 命令がどこにあるか調べてパスを通してください.
見つからない場合は最悪,以下のように全ファイルを調べれば見つかると思います.

この命令は管理者権限で (sudo) ,ルート (/) から全てのファイル(-type f) のうち,ファイル名が hoge のもの (-name “hoge”) を探す (find) という意味になります.

$ sudo find / -type f -name "hoge"

ほとんどの場合 /path/to/bin/hoge のような形だと思うので,以下のようにすればパスが通ると思います.

$ export PATH=$PATH:/path/to/bin

毎回これをするのは面倒なので,.bashrc.zshrc などに追記しておきましょう.

$ echo 'export PATH="$PATH:/path/to/bin"' >> ~/.bashrc

これで問題が解決することはないと思うので,次の 2.2. 実行できた場合 に進みましょう.

2.2. 実行できた場合

ユーザの環境では hoge が見つかっているが,PHP は見つけられないということになります.
つまり,PHP が使っている環境ではパスが通っていないということになります.

では,PHP のパスを確認してみましょう.
getenv に PATH という引数を与えることで,パスを取得してきてくれます.

echo(getenv("PATH"));

このようにして,再度ブラウザでボタンを押すと筆者と全く同じではないかもしれませんが,以下のようにパスが表示されているはずです.

/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

この中には /path/to/bin はないはずなので,これを足してあげれば動くはずです.
ここでは putenv を使ってパスを追加しています.

echo(getenv("PATH"));
echo('<br>');
$PATH = getenv("PATH");
putenv("PATH=$PATH:/path/to/bin");
echo(getenv("PATH"));

このようにしてみると以下のように表示されてパスが変更されたのがわかると思います.

/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/path/to/bin

再度試してみるとエラー出力がなくなって,0 が返ってきている (正常終了している) ことがわかると思います.

array(0) {
}
return: 0

後は,余計な出力を消せば完成です.

3. ModuleNotFoundError

test.phpにアクセスした時に Python の関数を実行しようとした時に起きる可能性のあるエラーです.
/path/to/hoge.py を実行することを考えてみます.

exec("python /path/to/hoge.py 2>&1",$dum,$rtn);

普通にローカル環境では動くのに,以下のようなエラーが出ることがあります.

array(4) {
  [0]=>
  string(34) "Traceback (most recent call last):"
  [1]=>
  string(70) "  File "/path/to/hoge.py", line 13, in "
  [2]=>
  string(34) "    import selenium"
  [3]=>
  string(47) "ModuleNotFoundError: No module named 'selenium'"
}
return: 1

つまり,selenium というモジュールは見つからないと言われているわけです.

3.1 実行されている Python を確認

まずは実行されている Python のパス確認しましょう.

exec("which python",$dum,$rtn);

ログの出力は以下のようになると思いますが,ローカルで which python を実効した結果と一致しているか確認してみてください.

array(1) {
  [0]=>
  string(16) "/usr/bin/python"
}
return: 0

一致していなければ(というかそもそも),実行する時に以下のようにフルパスで書いておくとよいです.

exec("/usr/bin/python /path/to/hoge.py 2>&1",$dum,$rtn);

3.2. Python が同じでも動かない

これで,同じ Python を実行しても動かない場合に集中できます.
ローカルではモジュールはあるのに,PHP で動かす時にはモジュールがないということなので,PHP からはアクセスできない場所にある,つまりパスが通っていないということになるはずです.

3.2.A. グローバルにモジュールをインストールする方法

簡単に解決する方法は,このモジュールをグローバルにインストールすることです.

$ sudo pip install selenium

これで,グローバルな環境にモジュールがインストールされたはずなので,PHP からモジュールが読み込めるはずです.

3.2.B. モジュールのパスを通す方法

一方,モジュールをグローバルにインストールするのは躊躇われるという場合もあるかと思います.
その場合はパスを通しましょう (この方法は筆者の場合は失敗しました…).
まずはモジュールの場所を調べないといけません.
ここで,ユーザのホームディレクトリを /path/to/home/ としておきます.

$ python
Python 3.7.3 (default, Jan 22 2021, 20:04:44)
[GCC 8.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import selenium
>>> print(selenium.__path__)
['/path/to/home/.local/lib/python3.7/site-packages/selenium']
>>> import sys
>>> print(sys.path)
['', '/usr/lib/python37.zip', '/usr/lib/python3.7', '/usr/lib/python3.7/lib-dynload', '/path/to/home/.local/lib/python3.7/site-packages', '/usr/local/lib/python3.7/dist-packages', '/usr/lib/python3/dist-packages']
>>> exit()

このようになっていたので,筆者の場合は /path/to/home/.local/lib/python3.7/site-packages にパスが通っていればいいということになり,実際 sys.path の結果に含まれていることがわかります.

一方,PHP でのパスを確認するために,path_test.py を作成して,実行します.

import sys
print(sys.path)
import selenium
exec("/usr/bin/python /path/to/path_test.py 2>&1",$dum,$rtn);

実行結果は以下のようになりました.

array(1) {
  [0]=>
  string(169) "['/path/to/home', '/usr/lib/python37.zip', '/usr/lib/python3.7', '/usr/lib/python3.7/lib-dynload', '/usr/local/lib/python3.7/dist-packages', '/usr/lib/python3/dist-packages']"
  [1]=>
  string(34) "Traceback (most recent call last):"
  [2]=>
  string(51) "  File "/path/to/path_test.py", line 3, in "
  [3]=>
  string(19) "    import selenium"
  [4]=>
  string(47) "ModuleNotFoundError: No module named 'selenium'"
}
return: 0

確かに,/path/to/home/.local/lib/python3.7/site-packages は含まれておらず,selenium も import できていません.
そこで,path_test.py を以下のように変更してみます.

import sys
# パスを追加
sys.path.append("/path/to/home/.local/lib/python3.7/site-packages")
print(sys.path)
import selenium

これでいける!!!!と思ったら無理でした…

array(5) {
  [0]=>
  string(216) "['/path/to/home', '/usr/lib/python37.zip', '/usr/lib/python3.7', '/usr/lib/python3.7/lib-dynload', '/usr/local/lib/python3.7/dist-packages', '/usr/lib/python3/dist-packages', '/path/to/home/.local/lib/python3.7/site-packages']"
  [1]=>
  string(34) "Traceback (most recent call last):"
  [2]=>
  string(52) "  File "/path/to/path_test.py", line 10, in "
  [3]=>
  string(19) "    import selenium"
  [4]=>
  string(47) "ModuleNotFoundError: No module named 'selenium'"
}
return: 1

しっかりとパスも追加されていますが,import には失敗しました.
原因を探っていると以下のことがわかりました.

$ cd /path/to/home
$ ls -la
drwx------  5 user user  4096 Jul  2  2020 .local

つまり,.local ディレクトリにアクセス権がありませんでした.
無理やり以下のようにアクセス権を与えてもいいのですが,セキュリティ上あまりよろしくなさそうなので,諦めました.

$ chmod -R 744 .local # これは少し危険です

3.2.C. モジュールをコピーしてくる方法

先程惜しいところまで行ったので諦めきれずに,path_test.py ファイルがあるディレクトリ /path/to/home にはパスが通っていることを利用して,そのディレクトリに selenium のディレクトリを持ってきました (強引).

$ cp -r /path/to/home/.local/lib/python3.7/site-packages/selenium /path/to/home
$ chmod -R 744 selenium

これで,めでたくアクセスできるようになりました!

import sys
print(sys.path)
import selenium
array(1) {
  [0]=>
  string(216) "['/path/to/home', '/usr/lib/python37.zip', '/usr/lib/python3.7', '/usr/lib/python3.7/lib-dynload', '/usr/local/lib/python3.7/dist-packages', '/usr/lib/python3/dist-packages']"
}
return: 0

権限のところで詰まってしまいましたね…
まだまだ修行が必要です.

4. まとめ

PHP の exec 関数を使って実行する際に起こりうるエラーの内詰まったものをいくつか解説しました.
環境などをいじるのは難しいですが,少しずつ慣れていきたいところですね.
PHP で sudo 命令を実行するというのもやってみたので,時間があればまた記事を書きたいと思います.
ぜひお楽しみに 👍

コメントを残す

メールアドレスが公開されることはありません。

CAPTCHA