Python から管理者権限でコマンドラインを実行する方法

Python のコード内からコマンドラインを実行する方法です。通常の権限と管理者権限とで分けて書いてあります。

ここで言う管理者権限とは、.py そのものを管理者権限で実行するのではなく、コード内からUAC (User Account Control; ユーザーアカウント制御) のダイアログを出してコマンドラインを実行することを指しています。


  1. 通常の権限での実行
  2. 管理者権限での実行
  3. Windows のサービスを開始したり停止したりするサンプルコード

1. 通常の権限での実行

通常の権限でコマンドラインを実行するには、subprocess モジュールの run 関数を使います。基本は、リスト化したコマンドを渡すだけです。

from subprocess import run

run(['cmd', '/c', 'echo', 'Hello', 'World.'])

cmd.exe に /c オプションを付け、残りのコマンドは空白箇所を区切りとして羅列しています。/c は処理後にコマンドプロンプトを終了するオプションで、これがないと正しく動作しません。この cmd /c の部分は、run の引数に shell=True を追加することで省略可能です。

run(['echo', 'Hello', 'World.'], shell=True)

もちろん以上のようなコードではエラーに対応できませんし、コマンドをいちいちクォーテーションで囲んで書くのも面倒ですから、実際には次のような形式にすると良いと思います。ここでは例として Hello World. を print してみました。

from subprocess import run
from traceback import print_exc

def hello_world():
    _args = 'cmd /c echo Hello World.'   # 実行するコマンド
    _output = run(
        _args.split(),                   # 空白区切りでリスト化
        capture_output=True, text=True,  # 出力をテキストで取得
    )
    _output.check_returncode()           # エラー時 CalledProcessError を送出
    return _output.stdout                # 標準出力を返す

def main():
    try:
        print(hello_world())             # Hello World.\n
    except:
        print_exc()

if __name__ == '__main__':
    main()

run 関数は返り値を取得することができます。上のコードではすべての出力をテキスト形式で _output に取得し、最終的に標準出力を取り出しています。出力 _output に check_returncode() を当てると、エラーがあった際に CalledProcessError を送出することができます。

コマンドラインで何を行うかによりますが、通常の権限で行えるものはほとんど run 関数とその返り値で処理できると思います。


2. 管理者権限での実行

次に、管理者権限で実行する方法です。ctypes ライブラリの windll を使います。この方法では実行結果が整数値で返ってくるため、文字列の表示に関しては標準出力ではなく自力でやっています。

from ctypes import windll

def runas_hello_world():
    _shell = windll.shell32.ShellExecuteW
    _args = '/c echo Hello World.'  # 実行するコマンド

    return _shell(
        None,     # 親 Window
        'runas',  # 管理者として実行
        'cmd',    # 実行するファイル (cmd.exe)
        _args,    # パラメータ
        None,     # デフォルトディレクトリ
        0,        # 0: 非表示, 1: 通常, 2: 最小化, 3: 最大化...
    )

def main():
    if runas_hello_world() > 32:    # 正常終了
        print('Hello World.')       # 自力で表示
    else:
        print('Some error occurred.')

if __name__ == '__main__':
    main()

ShellExecuteW というものを呼び出し、cmd.exe を管理者として実行しています。ShellExecuteW function (shellapi.h) に、引数などに関する情報が出ています。

上記のコードを実行すると、まずUACのダイアログが出てからコマンドが実行されます。正常に実行されたときは ShellExecuteW が32より大きい数値(原文:greater than 32)を返すようです。32がエラーなのか正常なのかが曖昧なのですが、上記のコードにおいては42が返ってきています。実際に使う際は正常とエラーとで返り値を確認した上で、コマンドの内容に合わせて処理を分岐することになります。

13行目の数値は、コマンドプロンプトの画面を出すかどうかを決めています。もし実行中に内容を表示させたいなら、1 を指定してください。


3. Window のサービスを開始したり停止したりするサンプルコード

通常権限および管理者権限でのコマンドライン実行が、両方とも出てくるようなサンプルコードを書いてみました。

Windows のサービスの開始や停止を行うもので、これは Windows に管理者としてログインしているかどうかに関わらず、UACダイアログでの確認が必要な処理です。初めにサービスの開始状況を通常権限の run で確認し、必要な場合にのみ ShellExecuteW で管理者として実行します。この例では、Spooler という印刷に関わるサービスを開始または停止できるようにしました。

#! /usr/bin/env python3 -I -S
"""Creative Commons CC0 <https://creativecommons.org/publicdomain/zero/1.0/legalcode>"""

from ctypes import windll, c_wchar_p
from re import ASCII, sub
from subprocess import run

# ------------------------------
def sanitize(strings):
    return sub(r'(\W)', r'^\1', strings, flags=ASCII)

# ------------------------------
def check_service(name) -> bool:
    _args = f'cmd /c sc query {sanitize(name)} | findstr STATE'
    _output = run(
        sub(r'(?<!\^) ', ',', _args).split(','),
        capture_output=True, text=True,  # Get output as text.
        timeout=5,                       # If the timeout expires, raise a TimeoutExpired.
    )
    _output.check_returncode()           # If returncode is non-zero, raise a CalledProcessError.
    return 'RUNNING' in _output.stdout

# ------------------------------
def runas_service(name, /, starting=True):
    try:
        _running = check_service(name)
    except Exception as err:
        print(f'{err.__class__.__name__}: {err}')
        return
    else:
        if _running and starting:
            print(f'{name} is already started.')
            return
        elif not _running and not starting:
            print(f'{name} is already stopped.')
            return

    _shell = windll.shell32.ShellExecuteW
    _start = 'start' if starting else 'stop'
    _args = f'/c sc {_start} {sanitize(name)}'
    _return_code = _shell(
        None,              # Parent window.
        'runas',           # Launches an application as Administrator.
        'cmd',             # The file on which to execute.
        c_wchar_p(_args),  # Parameters.
        None,              # Default directory.
        0,                 # 0: Hide, 1: Normal, 2: Minimized, 3: Maximized...
    )

    _not = 'not ' if _return_code <= 32 else ''
    _result = 'started' if starting else 'stopped'
    print(f'{name} is {_not}{_result}.')

# ------------------------------
if __name__ == '__main__':
    runas_service('Spooler', starting=True)  # When the service is to be started.
#    runas_service('Spooler', starting=False)  # When the service is to be suspended.

13行目の check_service() で、サービスが開始されているかどうかを確認しています。引数の name にはサービス名が入ります。コマンドは sc query を使用し、その標準出力の中に RUNNING の文字列があれば開始されていると判断します。ちなみに停止しているときの文字列は STOPPED です。

run にコマンドを渡す際、サービス名の name 変数を当てはめる前に、簡易的なサニタイジングを行っています。上記の例ではアスキー文字の [a-zA-Z0-9_] 以外の文字をすべてエスケープしています。セキュリティで考慮すべき点 を読んだところによると、Windows でなければ shlex モジュールの quote() が有用なようです。

コマンドは、エスケープされていない空白文字を ‘,’ に変換してから split でリスト化しています。str の split は、そのままだとエスケープの有無とは無関係に空白文字で区切ってしまうからです。

24行目の runas_service() が、サービスを開始したり停止したりする関数です。starting 引数が True だと開始されます。始めに check_service() でサービスの開始状況を確認し、先の処理が不要な場合にはメッセージを print して終了しています。

38行目からが管理者権限での処理で、sc start または sc stop コマンドでサービスの開始や停止を行います。今回の例ではやらなくても大丈夫なんですが、コマンドとして文字列を渡すときには c_wchar_p でC言語と互換性のあるデータ型に変換しておくと良いです。

あとは返り値によってメッセージが変わるように加工してから、結果を print しています。

Leave a comment

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

five × 4 =