รับรหัสทางออกของกระบวนการเบื้องหลัง


132

ฉันมีคำสั่ง CMD ที่เรียกจากสคริปต์ bourne เชลล์หลักของฉันซึ่งใช้เวลาตลอดไป

ฉันต้องการแก้ไขสคริปต์ดังนี้:

  1. รันคำสั่ง CMD แบบขนานเป็นกระบวนการพื้นหลัง ( CMD &)
  2. ในสคริปต์หลักมีลูปเพื่อตรวจสอบคำสั่งที่เกิดทุกสองสามวินาที ลูปยังสะท้อนข้อความบางส่วนเพื่อแสดงความคืบหน้าของสคริปต์
  3. ออกจากลูปเมื่อคำสั่ง spawned สิ้นสุดลง
  4. จับภาพและรายงานรหัสทางออกของกระบวนการสร้าง

ใครช่วยชี้แนะให้ฉันทำสิ่งนี้ให้สำเร็จได้ไหม


1
...และผู้ชนะคือ?
TrueY

1
@TrueY .. bob ไม่ได้เข้าระบบตั้งแต่วันที่เขาถามคำถาม เราไม่น่าจะรู้มาก่อน!
ghoti

คำตอบ:


130

1: ใน bash ให้$!ถือ PID ของกระบวนการพื้นหลังสุดท้ายที่ดำเนินการ ซึ่งจะบอกคุณว่าต้องตรวจสอบกระบวนการใด

4: wait <n>รอจนกว่ากระบวนการด้วย PID <n>จะเสร็จสมบูรณ์ (จะบล็อกจนกว่ากระบวนการจะเสร็จสมบูรณ์ดังนั้นคุณอาจไม่ต้องการเรียกสิ่งนี้จนกว่าคุณจะแน่ใจว่ากระบวนการเสร็จสิ้น) จากนั้นส่งคืนรหัสออกของกระบวนการที่เสร็จสมบูรณ์

2, 3: psหรือps | grep " $! "สามารถบอกคุณได้ว่ากระบวนการยังคงทำงานอยู่ ขึ้นอยู่กับคุณว่าจะทำความเข้าใจผลลัพธ์และตัดสินใจว่าจะจบอย่างไร ( ps | grepไม่ใช่เรื่องงี่เง่าหากคุณมีเวลาคุณสามารถหาวิธีที่มีประสิทธิภาพมากขึ้นในการบอกได้ว่ากระบวนการยังคงทำงานอยู่หรือไม่)

นี่คือสคริปต์โครงกระดูก:

# simulate a long process that will have an identifiable exit code
(sleep 15 ; /bin/false) &
my_pid=$!

while   ps | grep " $my_pid "     # might also need  | grep -v grep  here
do
    echo $my_pid is still in the ps output. Must still be running.
    sleep 3
done

echo Oh, it looks like the process is done.
wait $my_pid
# The variable $? always holds the exit code of the last command to finish.
# Here it holds the exit code of $my_pid, since wait exits with that code. 
my_status=$?
echo The exit status of the process was $my_status


54
kill -0 $!เป็นวิธีที่ดีกว่าในการบอกว่ากระบวนการยังทำงานอยู่หรือไม่ มันไม่ได้ส่งสัญญาณใด ๆ จริง ๆ เพียง แต่ตรวจสอบว่ากระบวนการยังมีชีวิตอยู่โดยใช้เชลล์ในตัวแทนที่จะเป็นกระบวนการภายนอก ตามที่man 2 killกล่าวว่า "ถ้าsigเป็น 0 จะไม่มีการส่งสัญญาณ แต่ยังคงดำเนินการตรวจสอบข้อผิดพลาดซึ่งสามารถใช้เพื่อตรวจสอบการมีอยู่ของ ID กระบวนการหรือ ID กลุ่มกระบวนการ"
ephemient

14
@ephemient kill -0จะส่งคืนค่าที่ไม่เป็นศูนย์หากคุณไม่ได้รับอนุญาตให้ส่งสัญญาณไปยังกระบวนการที่กำลังทำงานอยู่ น่าเสียดายที่ส่งคืน1ทั้งในกรณีนี้และกรณีที่ไม่มีกระบวนการ สิ่งนี้ทำให้มีประโยชน์เว้นแต่คุณจะไม่ได้เป็นเจ้าของกระบวนการซึ่งอาจเป็นไปได้แม้กระทั่งสำหรับกระบวนการที่คุณสร้างขึ้นหากมีเครื่องมือที่sudoเกี่ยวข้องหรือตั้งค่าไว้แล้ว (และอาจลดลง privs)
Craig Ringer

13
wait$?ไม่ได้กลับรหัสทางออกในตัวแปร เพียงส่งคืนรหัสออกและ$?เป็นรหัสออกของโปรแกรมเบื้องหน้าล่าสุด
MindlessRanger

7
สำหรับคนจำนวนมากที่โหวตkill -0. นี่คือการอ้างอิงที่ตรวจสอบโดยเพื่อนจาก SO ที่แสดงความคิดเห็นของ CraigRinger เป็นเรื่องที่ถูกต้อง: kill -0จะส่งคืนค่าที่ไม่ใช่ศูนย์สำหรับกระบวนการที่กำลังทำงานอยู่ ... แต่ps -pจะส่งคืน 0 เสมอสำหรับกระบวนการที่กำลังทำงานอยู่
Trevor Boyd Smith

58

นี่คือวิธีที่ฉันแก้ไขเมื่อฉันมีความต้องการที่คล้ายกัน:

# Some function that takes a long time to process
longprocess() {
        # Sleep up to 14 seconds
        sleep $((RANDOM % 15))
        # Randomly exit with 0 or 1
        exit $((RANDOM % 2))
}

pids=""
# Run five concurrent processes
for i in {1..5}; do
        ( longprocess ) &
        # store PID of process
        pids+=" $!"
done

# Wait for all processes to finish, will take max 14s
# as it waits in order of launch, not order of finishing
for p in $pids; do
        if wait $p; then
                echo "Process $p success"
        else
                echo "Process $p fail"
        fi
done

ฉันชอบแนวทางนี้
Kemin Zhou

ขอบคุณ! วิธีนี้ดูเหมือนจะง่ายที่สุดสำหรับฉัน
Luke Davis

4
โซลูชันนี้ไม่เป็นไปตามข้อกำหนด # 2: ลูปการตรวจสอบต่อกระบวนการเบื้องหลัง waits ทำให้สคริปต์รอจนกว่าจะสิ้นสุดกระบวนการ (แต่ละ)
DKroot

แนวทางที่เรียบง่ายและดี.. มีการค้นหาวิธีแก้ปัญหานี้มานานแล้ว ..
Santosh Kumar Arjunan

ไม่ได้ผล .. หรือไม่ทำตามที่คุณต้องการ: ไม่ได้ตรวจสอบสถานะการออกจากกระบวนการที่มีเบื้องหลัง?
conny

10

pid ของกระบวนการลูกที่มีพื้นหลังถูกเก็บไว้ใน$! . คุณสามารถจัดเก็บ pids ทุกกระบวนการที่เด็กเป็นอาร์เรย์เช่นPIDs []

wait [-n] [jobspec or pid …]

รอจนกว่ากระบวนการย่อยที่ระบุโดยแต่ละ ID กระบวนการ pid หรือ jobSpec ข้อมูลจำเพาะของงานออกและส่งคืนสถานะการออกของคำสั่งสุดท้ายที่รอ หากระบุข้อมูลจำเพาะของงานกระบวนการทั้งหมดในงานจะรอ หากไม่มีการให้อาร์กิวเมนต์กระบวนการย่อยที่แอ็คทีฟทั้งหมดจะถูกรอและสถานะการส่งคืนจะเป็นศูนย์ หากมีการระบุอ็อพชัน -n ให้รอจนกว่างานใด ๆ จะยุติและส่งคืนสถานะการออก หากทั้ง jobspec หรือ pid ไม่ได้ระบุกระบวนการย่อยที่แอ็คทีฟของเชลล์สถานะส่งคืนคือ 127

ใช้คำสั่งwaitคุณสามารถรอให้กระบวนการย่อยทั้งหมดเสร็จสิ้นในขณะที่คุณสามารถรับสถานะการออกของแต่ละกระบวนการย่อยผ่านทาง$? และสถานะการจัดเก็บลงในสถานะ [] จากนั้นคุณสามารถทำบางสิ่งได้ตามสถานะ

ฉันได้ลองใช้ 2 วิธีแก้ไขปัญหาต่อไปนี้แล้วและทำงานได้ดี solution01มีความกระชับมากขึ้นในขณะที่solution02นั้นซับซ้อนเล็กน้อย

solution01

#!/bin/bash

# start 3 child processes concurrently, and store each pid into array PIDS[].
process=(a.sh b.sh c.sh)
for app in ${process[@]}; do
  ./${app} &
  PIDS+=($!)
done

# wait for all processes to finish, and store each process's exit code into array STATUS[].
for pid in ${PIDS[@]}; do
  echo "pid=${pid}"
  wait ${pid}
  STATUS+=($?)
done

# after all processed finish, check their exit codes in STATUS[].
i=0
for st in ${STATUS[@]}; do
  if [[ ${st} -ne 0 ]]; then
    echo "$i failed"
  else
    echo "$i finish"
  fi
  ((i+=1))
done

solution02

#!/bin/bash

# start 3 child processes concurrently, and store each pid into array PIDS[].
i=0
process=(a.sh b.sh c.sh)
for app in ${process[@]}; do
  ./${app} &
  pid=$!
  PIDS[$i]=${pid}
  ((i+=1))
done

# wait for all processes to finish, and store each process's exit code into array STATUS[].
i=0
for pid in ${PIDS[@]}; do
  echo "pid=${pid}"
  wait ${pid}
  STATUS[$i]=$?
  ((i+=1))
done

# after all processed finish, check their exit codes in STATUS[].
i=0
for st in ${STATUS[@]}; do
  if [[ ${st} -ne 0 ]]; then
    echo "$i failed"
  else
    echo "$i finish"
  fi
  ((i+=1))
done

ฉันได้ลองและพิสูจน์แล้วว่าทำงานได้ดี คุณสามารถอ่านคำอธิบายของฉันในรหัส
Terry

โปรดอ่าน " ฉันจะเขียนคำตอบที่ดีได้อย่างไร " ซึ่งคุณจะพบข้อมูลต่อไปนี้: ... พยายามระบุข้อ จำกัด สมมติฐานหรือความเรียบง่ายในคำตอบของคุณ ความกะทัดรัดเป็นสิ่งที่ยอมรับได้ แต่คำอธิบายที่สมบูรณ์จะดีกว่า คำตอบของคุณจึงเป็นที่ยอมรับ แต่คุณมีโอกาสที่จะได้รับคะแนนโหวตสูงกว่ามากหากคุณสามารถอธิบายปัญหาและแนวทางแก้ไขได้อย่างละเอียด :-)
Noel Widmer

1
pid=$!; PIDS[$i]=${pid}; ((i+=1))สามารถเขียนได้ง่ายขึ้นเพราะต่อPIDS+=($!)ท้ายอาร์เรย์โดยไม่ต้องใช้ตัวแปรแยกสำหรับการจัดทำดัชนีหรือ pid เอง สิ่งเดียวกันนี้ใช้กับSTATUSอาร์เรย์
codeforester

1
@codeforester ขอบคุณสำหรับ sugesstion ของคุณฉันได้แก้ไขรหัส inital ของฉันเป็น solution01 มันดูกระชับมากขึ้น
Terry

เช่นเดียวกันกับที่อื่น ๆ ที่คุณเพิ่มสิ่งต่างๆลงในอาร์เรย์
codeforester

8

ตามที่ฉันเห็นคำตอบเกือบทั้งหมดใช้ยูทิลิตี้ภายนอก (ส่วนใหญ่ps) เพื่อสำรวจสถานะของกระบวนการเบื้องหลัง มีโซลูชัน unixesh มากขึ้นจับสัญญาณ SIGCHLD ในเครื่องจัดการสัญญาณจะต้องมีการตรวจสอบว่ากระบวนการลูกใดหยุดทำงาน สามารถทำได้โดยkill -0 <PID>บิวท์อิน (สากล) หรือตรวจสอบการมีอยู่ของ/proc/<PID>ไดเร็กทอรี (เฉพาะ Linux) หรือใช้jobsบิวท์อิน (โดยเฉพาะ jobs -lรายงาน pid ด้วย ในกรณีนี้ช่องที่ 3 ของเอาต์พุตสามารถหยุดได้ | วิ่ง | เสร็จสิ้น | ออก )

นี่คือตัวอย่างของฉัน

loop.shขั้นตอนการเปิดตัวที่เรียกว่า ยอมรับ-xหรือตัวเลขเป็นอาร์กิวเมนต์ สำหรับ-xคือออกด้วยรหัสทางออก 1 สำหรับหมายเลขจะรอ num * 5 วินาที ทุกๆ 5 วินาทีจะพิมพ์ PID

เรียกกระบวนการเรียกใช้launch.sh:

#!/bin/bash

handle_chld() {
    local tmp=()
    for((i=0;i<${#pids[@]};++i)); do
        if [ ! -d /proc/${pids[i]} ]; then
            wait ${pids[i]}
            echo "Stopped ${pids[i]}; exit code: $?"
        else tmp+=(${pids[i]})
        fi
    done
    pids=(${tmp[@]})
}

set -o monitor
trap "handle_chld" CHLD

# Start background processes
./loop.sh 3 &
pids+=($!)
./loop.sh 2 &
pids+=($!)
./loop.sh -x &
pids+=($!)

# Wait until all background processes are stopped
while [ ${#pids[@]} -gt 0 ]; do echo "WAITING FOR: ${pids[@]}"; sleep 2; done
echo STOPPED

สำหรับคำอธิบายเพิ่มเติมโปรดดู: การเริ่มต้นกระบวนการจากสคริปต์ทุบตีล้มเหลว


1
เนื่องจากเรากำลังพูดถึง Bash ดังนั้น for loop จึงสามารถเขียนเป็น: for i in ${!pids[@]};โดยใช้การขยายพารามิเตอร์
PlasmaBinturong

8
#/bin/bash

#pgm to monitor
tail -f /var/log/messages >> /tmp/log&
# background cmd pid
pid=$!
# loop to monitor running background cmd
while :
do
    ps ax | grep $pid | grep -v grep
    ret=$?
    if test "$ret" != "0"
    then
        echo "Monitored pid ended"
        break
    fi
    sleep 5

done

wait $pid
echo $?

2
นี่คือเคล็ดลับในการหลีกเลี่ยงไฟล์grep -v. คุณสามารถ จำกัด การค้นหาไว้ที่จุดเริ่มต้นของบรรทัด: grep '^'$pidนอกจากนี้คุณสามารถทำได้ps p $pid -o pid=อย่างไรก็ตาม นอกจากนี้tail -fจะไม่จบลงจนกว่าคุณจะฆ่ามันดังนั้นฉันไม่คิดว่ามันเป็นวิธีที่ดีในการสาธิตสิ่งนี้ (อย่างน้อยก็ไม่ต้องชี้ให้เห็น) คุณอาจต้องการเปลี่ยนทิศทางผลลัพธ์ของpsคำสั่งของคุณ/dev/nullหรือมันจะไปที่หน้าจอทุกครั้งที่ทำซ้ำ exitสาเหตุของคุณที่waitจะข้ามไป - น่าจะเป็นbreakไฟล์. แต่ไม่ใช่while/ psและwaitซ้ำซ้อน?
หยุดชั่วคราวจนกว่าจะมีประกาศอีกครั้ง

5
ทำไมทุกคนถึงลืมkill -0 $pid? มันไม่ได้ส่งสัญญาณใด ๆ จริง ๆ เพียง แต่ตรวจสอบว่ากระบวนการนั้นยังมีชีวิตอยู่โดยใช้เชลล์ในตัวแทนที่จะเป็นกระบวนการภายนอก
ephemient

3
เพราะคุณสามารถฆ่ากระบวนการที่คุณเป็นเจ้าของได้เท่านั้น:bash: kill: (1) - Operation not permitted
errant.info

2
การวนซ้ำซ้ำซ้อน รอสักครู่. รหัสน้อย => กรณีขอบน้อย
Brais Gabin

@Brais Gabin ลูปการตรวจสอบเป็นข้อกำหนด # 2 ของคำถาม
DKroot

5

ฉันจะเปลี่ยนแนวทางของคุณเล็กน้อย แทนที่จะตรวจสอบทุกสองสามวินาทีว่าคำสั่งยังคงมีชีวิตอยู่หรือไม่และรายงานข้อความให้มีกระบวนการอื่นที่รายงานทุกสองสามวินาทีว่าคำสั่งยังคงทำงานอยู่จากนั้นจึงฆ่ากระบวนการนั้นเมื่อคำสั่งเสร็จสิ้น ตัวอย่างเช่น:

#! / bin / ดวลจุดโทษ

cmd () {นอน 5; ทางออก 24; }

cmd & # เรียกใช้กระบวนการที่ยาวนาน
pid = $! # บันทึกพีด

# วางไข่กระบวนการที่รายงานโดยปกติว่าคำสั่งยังคงทำงานอยู่
ในขณะที่ echo "$ (date): $ pid ยังทำงานอยู่"; นอน 1; เสร็จแล้ว &
echoer = $!

# วางกับดักเพื่อฆ่านักข่าวเมื่อกระบวนการเสร็จสิ้น
กับดัก 'kill $ echoer' 0

# รอให้กระบวนการเสร็จสิ้น
ถ้ารอ $ pid; แล้วก็
    echo "cmd สำเร็จ"
อื่น
    echo "cmd FAILED !! (ส่งคืน $?)"
Fi

แม่แบบที่ดีขอบคุณสำหรับการแบ่งปัน! ฉันเชื่อว่าแทนที่จะเป็นกับดักเราสามารถทำได้while kill -0 $pid 2> /dev/null; do X; doneหวังว่ามันจะเป็นประโยชน์สำหรับคนอื่นในอนาคตที่อ่านข้อความนี้)
punkbit

3

ทีมของเรามีความต้องการเดียวกันกับสคริปต์ที่เรียกใช้ SSH จากระยะไกลซึ่งหมดเวลาหลังจากไม่มีการใช้งาน 25 นาที นี่คือวิธีแก้ปัญหาด้วยลูปการตรวจสอบที่ตรวจสอบกระบวนการเบื้องหลังทุก ๆ วินาที แต่จะพิมพ์ทุกๆ 10 นาทีเท่านั้นเพื่อยับยั้งการหมดเวลาของการไม่ใช้งาน

long_running.sh & 
pid=$!

# Wait on a background job completion. Query status every 10 minutes.
declare -i elapsed=0
# `ps -p ${pid}` works on macOS and CentOS. On both OSes `ps ${pid}` works as well.
while ps -p ${pid} >/dev/null; do
  sleep 1
  if ((++elapsed % 600 == 0)); then
    echo "Waiting for the completion of the main script. $((elapsed / 60))m and counting ..."
  fi
done

# Return the exit code of the terminated background process. This works in Bash 4.4 despite what Bash docs say:
# "If neither jobspec nor pid specifies an active child process of the shell, the return status is 127."
wait ${pid}

2

ตัวอย่างง่ายๆคล้ายกับวิธีแก้ปัญหาข้างต้น สิ่งนี้ไม่ต้องการการตรวจสอบผลลัพธ์ของกระบวนการใด ๆ ตัวอย่างถัดไปใช้ tail เพื่อติดตามเอาต์พุต

$ echo '#!/bin/bash' > tmp.sh
$ echo 'sleep 30; exit 5' >> tmp.sh
$ chmod +x tmp.sh
$ ./tmp.sh &
[1] 7454
$ pid=$!
$ wait $pid
[1]+  Exit 5                  ./tmp.sh
$ echo $?
5

ใช้ tail เพื่อติดตามผลลัพธ์ของกระบวนการและออกเมื่อกระบวนการเสร็จสมบูรณ์

$ echo '#!/bin/bash' > tmp.sh
$ echo 'i=0; while let "$i < 10"; do sleep 5; echo "$i"; let i=$i+1; done; exit 5;' >> tmp.sh
$ chmod +x tmp.sh
$ ./tmp.sh
0
1
2
^C
$ ./tmp.sh > /tmp/tmp.log 2>&1 &
[1] 7673
$ pid=$!
$ tail -f --pid $pid /tmp/tmp.log
0
1
2
3
4
5
6
7
8
9
[1]+  Exit 5                  ./tmp.sh > /tmp/tmp.log 2>&1
$ wait $pid
$ echo $?
5

1

อีกวิธีหนึ่งคือการตรวจสอบกระบวนการผ่านระบบไฟล์ proc (ปลอดภัยกว่า ps / grep combo) เมื่อคุณเริ่มกระบวนการมีโฟลเดอร์ที่เกี่ยวข้องใน / proc / $ pid ดังนั้นวิธีแก้ปัญหาอาจเป็นได้

#!/bin/bash
....
doSomething &
local pid=$!
while [ -d /proc/$pid ]; do # While directory exists, the process is running
    doSomethingElse
    ....
else # when directory is removed from /proc, process has ended
    wait $pid
    local exit_status=$?
done
....

ตอนนี้คุณสามารถใช้ตัวแปร $ exit_status ได้ตามต้องการ


ไม่ทำงานใน bash? Syntax error: "else" unexpected (expecting "done")
benjaoming

1

ด้วยวิธีนี้สคริปต์ของคุณไม่ต้องรอกระบวนการเบื้องหลังคุณจะต้องตรวจสอบไฟล์ชั่วคราวสำหรับสถานะการออกเท่านั้น

FUNCmyCmd() { sleep 3;return 6; };

export retFile=$(mktemp); 
FUNCexecAndWait() { FUNCmyCmd;echo $? >$retFile; }; 
FUNCexecAndWait&

ตอนนี้สคริปต์ของคุณสามารถทำอย่างอื่นได้ในขณะที่คุณต้องคอยตรวจสอบเนื้อหาของ retFile (อาจมีข้อมูลอื่น ๆ ที่คุณต้องการเช่นเวลาออก)

PS: btw ฉันเขียนโค้ดคิดเป็นทุบตี


1

วิธีแก้ปัญหาของฉันคือใช้ไปป์ที่ไม่ระบุชื่อเพื่อส่งผ่านสถานะไปยังลูปการตรวจสอบ ไม่มีไฟล์ชั่วคราวที่ใช้ในการแลกเปลี่ยนสถานะจึงไม่มีการล้างข้อมูล หากคุณไม่แน่ใจเกี่ยวกับจำนวนงานเบื้องหลังเงื่อนไขการหยุดพักอาจเป็น[ -z "$(jobs -p)" ]ได้

#!/bin/bash

exec 3<> <(:)

{ sleep 15 ; echo "sleep/exit $?" >&3 ; } &

while read -u 3 -t 1 -r STAT CODE || STAT="timeout" ; do
    echo "stat: ${STAT}; code: ${CODE}"
    if [ "${STAT}" = "sleep/exit" ] ; then
        break
    fi
done

0

สิ่งนี้อาจเกินกว่าคำถามของคุณอย่างไรก็ตามหากคุณกังวลเกี่ยวกับระยะเวลาที่กำลังดำเนินการอยู่คุณอาจสนใจที่จะตรวจสอบสถานะของกระบวนการทำงานเบื้องหลังหลังจากช่วงเวลาหนึ่ง ง่ายพอที่จะตรวจสอบว่า PID ของลูกใดยังคงใช้งานอยู่pgrep -P $$อย่างไรก็ตามฉันได้คิดวิธีแก้ไขปัญหาต่อไปนี้เพื่อตรวจสอบสถานะการออกของ PID ที่หมดอายุแล้ว:

cmd1() { sleep 5; exit 24; }
cmd2() { sleep 10; exit 0; }

pids=()
cmd1 & pids+=("$!")
cmd2 & pids+=("$!")

lasttimeout=0
for timeout in 2 7 11; do
  echo -n "interval-$timeout: "
  sleep $((timeout-lasttimeout))

  # you can only wait on a pid once
  remainingpids=()
  for pid in ${pids[*]}; do
     if ! ps -p $pid >/dev/null ; then
        wait $pid
        echo -n "pid-$pid:exited($?); "
     else
        echo -n "pid-$pid:running; "
        remainingpids+=("$pid")
     fi
  done
  pids=( ${remainingpids[*]} )

  lasttimeout=$timeout
  echo
done

ซึ่งผลลัพธ์:

interval-2: pid-28083:running; pid-28084:running; 
interval-7: pid-28083:exited(24); pid-28084:running; 
interval-11: pid-28084:exited(0); 

หมายเหตุ: คุณสามารถเปลี่ยน$pidsเป็นตัวแปรสตริงแทนอาร์เรย์เพื่อลดความซับซ้อนของสิ่งต่างๆได้หากต้องการ

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.