tee + cat: ใช้เอาต์พุตหลายครั้งแล้วต่อผลลัพธ์ที่ต่อกัน


18

ถ้าผมเรียกคำสั่งบางอย่างเช่นechoฉันสามารถใช้ผลจากคำสั่งว่าในคำสั่งอื่น ๆ teeที่มีหลาย ตัวอย่าง:

echo "Hello world!" | tee >(command1) >(command2) >(command3)

ด้วย cat ฉันสามารถรวบรวมผลลัพธ์ของคำสั่งต่าง ๆ ได้ ตัวอย่าง:

cat <(command1) <(command2) <(command3)

ฉันต้องการที่จะสามารถทำทั้งสองสิ่งในเวลาเดียวกันเพื่อที่จะสามารถteeเรียกคำสั่งเหล่านั้นในผลลัพธ์ของสิ่งอื่น (ตัวอย่างเช่นที่echoฉันเขียน) แล้วรวบรวมผลลัพธ์ทั้งหมดของพวกเขาในผลลัพธ์เดียวด้วยcat.

มันเป็นสิ่งสำคัญที่จะให้ผลในการสั่งซื้อที่นี้หมายถึงเส้นในการส่งออกของcommand1, command2และcommand3ไม่ควรพัน แต่ได้รับคำสั่งเป็นคำสั่งที่มี (ที่มันเกิดขึ้นกับcat)

อาจจะมีตัวเลือกที่ดีกว่าcatและteeแต่ผู้ที่เป็นคนที่ฉันรู้เพื่อให้ห่างไกล

ฉันต้องการหลีกเลี่ยงการใช้ไฟล์ชั่วคราวเนื่องจากขนาดของอินพุตและเอาต์พุตอาจมีขนาดใหญ่

ฉันจะทำสิ่งนี้ได้อย่างไร

PD: ปัญหาอื่นคือสิ่งนี้เกิดขึ้นในลูปซึ่งทำให้การจัดการไฟล์ชั่วคราวหนักขึ้น นี่คือรหัสปัจจุบันที่ฉันมีและใช้งานได้กับ testcase เล็ก ๆ แต่มันสร้างลูปไม่สิ้นสุดเมื่ออ่านและเขียนจาก auxfile ด้วยวิธีที่ฉันไม่เข้าใจ

somefunction()
{
  if [ $1 -eq 1 ]
  then
    echo "Hello world!"
  else
    somefunction $(( $1 - 1 )) > auxfile
    cat <(command1 < auxfile) \
        <(command2 < auxfile) \
        <(command3 < auxfile)
  fi
}

การอ่านและการเขียนใน auxfile ดูเหมือนจะทับซ้อนกันทำให้ทุกอย่างเกิดการระเบิด


2
เราคุยกันใหญ่แค่ไหน ความต้องการของคุณบังคับให้ทุกอย่างถูกเก็บไว้ในหน่วยความจำ การเก็บผลลัพธ์ตามลำดับหมายความว่า command1 ต้องดำเนินการให้เสร็จก่อน (ดังนั้นจึงสามารถอ่านอินพุตทั้งหมดและพิมพ์เอาต์พุตทั้งหมด) ก่อนที่ command2 และ command3 สามารถเริ่มต้นการประมวลผลได้
frostschutz

คุณถูกต้องอินพุตและเอาต์พุตของ command2 และ command3 นั้นใหญ่เกินไปที่จะเก็บไว้ในหน่วยความจำ ฉันคาดหวังว่าการใช้ swap จะทำงานได้ดีกว่าการใช้ไฟล์ชั่วคราว ปัญหาอีกประการหนึ่งที่ฉันมีคือสิ่งนี้เกิดขึ้นในลูปและทำให้การจัดการไฟล์ทำได้ยากขึ้น ฉันกำลังใช้ไฟล์เดียว แต่ในขณะนี้ด้วยเหตุผลบางอย่างมีบางอย่างทับซ้อนกันในการอ่านและเขียนจากไฟล์ที่ทำให้มันเติบโตขึ้น infinitum โฆษณา ฉันจะพยายามอัปเดตคำถามโดยไม่ทำให้คุณมีรายละเอียดมากเกินไป
Trylks

4
คุณต้องใช้ไฟล์ชั่วคราว ทั้งสำหรับการป้อนข้อมูลหรือการส่งออกecho HelloWorld > file; (command1<file;command2<file;command3<file) echo | tee cmd1 cmd2 cmd3; cat cmd1-output cmd2-output cmd3-outputนั่นเป็นวิธีการทำงาน - ทีสามารถแยกอินพุตได้ก็ต่อเมื่อคำสั่งทั้งหมดทำงานและประมวลผลแบบขนาน ถ้าใครนอนคำสั่ง (เพราะคุณไม่ต้องการ interleaving) มันก็จะปิดกั้นคำสั่งทั้งหมดเพื่อป้องกันไม่ให้กรอกข้อมูลหน่วยความจำด้วยการป้อนข้อมูล ...
frostschutz

คำตอบ:


27

คุณสามารถใช้การรวมกันของ GNU stdbuf และpeeจากmoreutils :

echo "Hello world!" | stdbuf -o 1M pee cmd1 cmd2 cmd3 > output

pee popen(3)s บรรทัดคำสั่งเชลล์ทั้งสามจากนั้นfreadป้อนข้อมูลและfwriteทั้งสามบรรทัดซึ่งจะถูกบัฟเฟอร์สูงถึง 1M

แนวคิดคือมีบัฟเฟอร์อย่างน้อยใหญ่เท่ากับอินพุต วิธีนี้แม้ว่าคำสั่งทั้งสามจะเริ่มขึ้นในเวลาเดียวกัน แต่ก็จะเห็นเฉพาะอินพุตที่เข้ามาเมื่อpee pcloseคำสั่งทั้งสามนั้นเรียงตามลำดับ

เมื่อแต่ละpclose, peeวูบวาบบัฟเฟอร์คำสั่งและรอสำหรับการยกเลิกของมัน สิ่งนี้รับประกันได้ว่าตราบใดที่cmdxคำสั่งเหล่านั้นไม่ได้เริ่มต้นแสดงผลอะไรก่อนที่พวกเขาจะได้รับอินพุตใด ๆ (และอย่าแยกกระบวนการที่อาจส่งออกต่อไปหลังจากที่ผู้ปกครองส่งคืน) ผลลัพธ์ของคำสั่งทั้งสามจะไม่ บรรณนิทัศน์

ผลก็เหมือนกับการใช้ไฟล์ temp ในหน่วยความจำโดยมีข้อเสียคือคำสั่ง 3 คำสั่งจะเริ่มพร้อมกัน

เพื่อหลีกเลี่ยงการเริ่มต้นคำสั่งพร้อมกันคุณสามารถเขียนpeeเป็นฟังก์ชั่นเปลือก:

pee() (
  input=$(cat; echo .)
  for i do
    printf %s "${input%.}" | eval "$i"
  done
)
echo "Hello world!" | pee cmd1 cmd2 cmd3 > out

แต่ระวังว่าเชลล์อื่น ๆ นอกจากzshจะล้มเหลวสำหรับอินพุตไบนารีที่มีอักขระ NUL

หลีกเลี่ยงการใช้ไฟล์ชั่วคราว แต่นั่นหมายถึงอินพุตทั้งหมดถูกเก็บไว้ในหน่วยความจำ

ไม่ว่าในกรณีใดคุณจะต้องเก็บอินพุตไว้ในหน่วยความจำหรือไฟล์ชั่วคราว

จริงๆแล้วมันเป็นคำถามที่น่าสนใจเพราะมันแสดงให้เราเห็นถึงขีด จำกัด ของแนวคิดของ Unix ที่มีเครื่องมือง่ายๆหลายอย่างที่ให้ความร่วมมือกับงานเดียว

ที่นี่เราต้องการมีเครื่องมือหลายอย่างที่ให้ความร่วมมือกับงาน:

  • คำสั่ง source (ที่นี่echo)
  • คำสั่งโปรแกรมเลือกจ่ายงาน ( tee)
  • คำสั่งบางตัวกรอง ( cmd1, cmd2, cmd3)
  • และคำสั่งการรวม ( cat)

มันจะดีถ้าพวกเขาสามารถทำงานร่วมกันในเวลาเดียวกันและทำงานอย่างหนักกับข้อมูลที่พวกเขาตั้งใจจะประมวลผลทันทีที่พร้อมใช้งาน

ในกรณีของคำสั่งตัวกรองเดียวมันง่าย:

src | tee | cmd1 | cat

คำสั่งทั้งหมดจะทำงานพร้อมกันcmd1เริ่มเคี้ยวข้อมูลจากsrcทันทีที่พร้อมใช้งาน

ตอนนี้ด้วยคำสั่งตัวกรองสามคำเรายังคงสามารถทำเช่นเดิม: เริ่มต้นพร้อมกันและเชื่อมต่อพวกเขาด้วยไพพ์:

               ┏━━━┓▁▁▁▁▁▁▁▁▁▁┏━━━━┓▁▁▁▁▁▁▁▁▁▁┏━━━┓
               ┃   ┃░░░░2░░░░░┃cmd1┃░░░░░5░░░░┃   ┃
               ┃   ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃   ┃
┏━━━┓▁▁▁▁▁▁▁▁▁▁┃   ┃▁▁▁▁▁▁▁▁▁▁┏━━━━┓▁▁▁▁▁▁▁▁▁▁┃   ┃▁▁▁▁▁▁▁▁▁┏━━━┓
┃src┃░░░░1░░░░░┃tee┃░░░░3░░░░░┃cmd2┃░░░░░6░░░░┃cat┃░░░░░░░░░┃out┃
┗━━━┛▔▔▔▔▔▔▔▔▔▔┃   ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃   ┃▔▔▔▔▔▔▔▔▔┗━━━┛
               ┃   ┃▁▁▁▁▁▁▁▁▁▁┏━━━━┓▁▁▁▁▁▁▁▁▁▁┃   ┃
               ┃   ┃░░░░4░░░░░┃cmd3┃░░░░░7░░░░┃   ┃
               ┗━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━┛

ซึ่งเราสามารถทำได้ค่อนข้างง่ายกับท่อชื่อ :

pee() (
  mkfifo tee-cmd1 tee-cmd2 tee-cmd3 cmd1-cat cmd2-cat cmd3-cat
  { tee tee-cmd1 tee-cmd2 tee-cmd3 > /dev/null <&3 3<&- & } 3<&0
  eval "$1 < tee-cmd1 1<> cmd1-cat &"
  eval "$2 < tee-cmd2 1<> cmd2-cat &"
  eval "$3 < tee-cmd3 1<> cmd3-cat &"
  exec cat cmd1-cat cmd2-cat cmd3-cat
)
echo abc | pee 'tr a A' 'tr b B' 'tr c C'

(เหนือ} 3<&0คือเพื่อหลีกเลี่ยงความจริงที่&เปลี่ยนเส้นทางstdinจาก/dev/nullและเราใช้<>เพื่อหลีกเลี่ยงการเปิดท่อเพื่อปิดกั้นจนกว่าจะสิ้นสุดอีก ( cat) ได้เปิดเช่นกัน)

หรือเพื่อหลีกเลี่ยงการตั้งชื่อไปป์อีกเล็กน้อยด้วยzshcoproc:

pee() (
  n=0 ci= co= is=() os=()
  for cmd do
    eval "coproc $cmd $ci $co"

    exec {i}<&p {o}>&p
    is+=($i) os+=($o)
    eval i$n=$i o$n=$o
    ci+=" {i$n}<&-" co+=" {o$n}>&-"
    ((n++))
  done
  coproc :
  read -p
  eval tee /dev/fd/$^os $ci "> /dev/null &" exec cat /dev/fd/$^is $co
)
echo abc | pee 'tr a A' 'tr b B' 'tr c C'

ตอนนี้คำถามคือเมื่อโปรแกรมทั้งหมดเริ่มต้นและเชื่อมต่อแล้วการไหลของข้อมูลจะเป็นอย่างไร

เรามีข้อ จำกัด สองข้อ:

  • tee ฟีดเอาท์พุททั้งหมดในอัตราเดียวกันดังนั้นมันสามารถส่งข้อมูลในอัตราของท่อส่งออกที่ช้าที่สุดเท่านั้น
  • cat จะเริ่มอ่านจากไปป์ที่สองเท่านั้น (ไพพ์ 6 ในรูปวาดข้างบน) เมื่อข้อมูลทั้งหมดถูกอ่านจากครั้งแรก (5)

นั่นหมายความว่าข้อมูลจะไม่ไหลในท่อ 6 จนกว่าจะcmd1เสร็จสิ้น และเช่นเดียวกับในกรณีtr b Bข้างต้นนั่นอาจหมายความว่าข้อมูลจะไม่ไหลในไพพ์ 3 ซึ่งหมายความว่าจะไม่ไหลในไพพ์ 2, 3 หรือ 4 เนื่องจากteeฟีดที่อัตราช้าที่สุดของทั้ง 3

ในทางปฏิบัติไพพ์เหล่านี้มีขนาดที่ไม่เป็นศูนย์ดังนั้นข้อมูลบางอย่างจะจัดการเพื่อให้ผ่านและในระบบของฉันอย่างน้อยฉันก็สามารถทำให้มันทำงานได้ถึง:

yes abc | head -c $((2 * 65536 + 8192)) | pee 'tr a A' 'tr b B' 'tr c C' | uniq -c -c

นอกเหนือจากนั้นด้วย

yes abc | head -c $((2 * 65536 + 8192 + 1)) | pee 'tr a A' 'tr b B' 'tr c C' | uniq -c

เรามีการหยุดชะงักโดยที่เราอยู่ในสถานการณ์นี้:

               ┏━━━┓▁▁▁▁2▁▁▁▁▁┏━━━━┓▁▁▁▁▁5▁▁▁▁┏━━━┓
               ┃   ┃░░░░░░░░░░┃cmd1┃░░░░░░░░░░┃   ┃
               ┃   ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃   ┃
┏━━━┓▁▁▁▁1▁▁▁▁▁┃   ┃▁▁▁▁3▁▁▁▁▁┏━━━━┓▁▁▁▁▁6▁▁▁▁┃   ┃▁▁▁▁▁▁▁▁▁┏━━━┓
┃src┃██████████┃tee┃██████████┃cmd2┃██████████┃cat┃░░░░░░░░░┃out┃
┗━━━┛▔▔▔▔▔▔▔▔▔▔┃   ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃   ┃▔▔▔▔▔▔▔▔▔┗━━━┛
               ┃   ┃▁▁▁▁4▁▁▁▁▁┏━━━━┓▁▁▁▁▁7▁▁▁▁┃   ┃
               ┃   ┃██████████┃cmd3┃██████████┃   ┃
               ┗━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━┛

เราได้ใส่ท่อ 3 และ 6 (แต่ละ 64kiB) teeได้อ่านไบต์พิเศษนั้นมันได้ป้อนเข้าไปcmd1แต่

  • ตอนนี้มันถูกบล็อกการเขียนบนไพพ์ 3 เนื่องจากมันกำลังรอcmd2ให้มันว่างเปล่า
  • cmd2ไม่สามารถว่างเปล่าเพราะมันถูกบล็อกเขียนในท่อ 6 รอcatเพื่อมันว่างเปล่า
  • cat ไม่สามารถล้างข้อมูลได้เนื่องจากกำลังรอจนกว่าจะไม่มีอินพุตเพิ่มเติมในไพพ์ 5
  • cmd1ไม่สามารถบอกได้ว่ามีการป้อนข้อมูลไม่มากเพราะมันกำลังรอตัวเองสำหรับการป้อนข้อมูลเพิ่มเติมจากcattee
  • และteeไม่สามารถบอกได้ว่าcmd1ไม่มีข้อมูลป้อนเข้าเพิ่มเติมเนื่องจากถูกบล็อก ... และต่อไป

เรามีห่วงพึ่งพาและทำให้การหยุดชะงัก

ตอนนี้ทางออกคืออะไร ท่อที่ใหญ่กว่า 3 และ 4 (ใหญ่พอที่จะมีsrcเอาท์พุททั้งหมด) จะทำเช่นนั้น เราสามารถทำเช่นนั้นได้โดยการแทรกpv -qB 1Gระหว่างteeและcmd2/3ตำแหน่งที่pvสามารถเก็บข้อมูลได้มากถึง 1G ที่รอcmd2และcmd3อ่าน นั่นหมายความว่าสองสิ่งแม้ว่า:

  1. ที่ใช้หน่วยความจำจำนวนมากและยิ่งกว่านั้นก็คือการทำซ้ำ
  2. นั่นคือความล้มเหลวในการมีคำสั่งทั้ง 3 คำที่ให้ความร่วมมือเพราะcmd2ในความเป็นจริงแล้วจะเริ่มดำเนินการกับข้อมูลเมื่อ cmd1 เสร็จสิ้นเท่านั้น

วิธีแก้ไขปัญหาที่สองคือทำให้ท่อ 6 และ 7 ใหญ่ขึ้นเช่นกัน สมมติว่าcmd2และcmd3สร้างเอาต์พุตมากที่สุดเท่าที่ใช้งานซึ่งจะไม่ใช้หน่วยความจำมากขึ้น

วิธีเดียวที่จะหลีกเลี่ยงการทำซ้ำข้อมูล (ในปัญหาแรก) จะใช้การเก็บรักษาข้อมูลในตัวแจกจ่ายที่ใช้การเปลี่ยนแปลงteeที่สามารถฟีดข้อมูลในอัตราการส่งออกที่เร็วที่สุด (การเก็บข้อมูลเพื่อป้อนข้อมูล ช้าลงตามความเร็วของตัวเอง) ไม่น่ารำคาญจริงๆ

ดังนั้นในที่สุดสิ่งที่ดีที่สุดที่เราสามารถรับได้โดยไม่ต้องเขียนโปรแกรมอาจเป็นสิ่งที่ต้องการ (ไวยากรณ์ Zsh):

max_hold=1G
pee() (
  n=0 ci= co= is=() os=()
  for cmd do
    if ((n)); then
      eval "coproc pv -qB $max_hold $ci $co | $cmd $ci $co | pv -qB $max_hold $ci $co"
    else
      eval "coproc $cmd $ci $co"
    fi

    exec {i}<&p {o}>&p
    is+=($i) os+=($o)
    eval i$n=$i o$n=$o
    ci+=" {i$n}<&-" co+=" {o$n}>&-"
    ((n++))
  done
  coproc :
  read -p
  eval tee /dev/fd/$^os $ci "> /dev/null &" exec cat /dev/fd/$^is $co
)
yes abc | head -n 1000000 | pee 'tr a A' 'tr b B' 'tr c C' | uniq -c

คุณถูกต้องการหยุดชะงักเป็นปัญหาที่ใหญ่ที่สุดที่ฉันเคยพบมาเพื่อหลีกเลี่ยงการใช้ไฟล์ชั่วคราว ดูเหมือนว่าไฟล์เหล่านี้จะค่อนข้างเร็ว แต่ฉันไม่รู้ว่าไฟล์เหล่านี้ถูกเก็บไว้ที่ใดที่หนึ่งฉันกลัวเวลาในการเข้าถึงดิสก์ แต่พวกเขาดูสมเหตุสมผลแล้ว
Trylks

6
พิเศษ+1 สำหรับศิลปะ ASCII ที่ดี :-)
Kurt Pfeifle

3

สิ่งที่คุณเสนอไม่สามารถทำได้อย่างง่ายดายด้วยคำสั่งใด ๆ ที่มีอยู่และไม่สมเหตุสมผลนัก ความคิดทั้งหมดของท่อ ( |ใน Unix / Linux) ที่อยู่ในเขียนผลลัพธ์ (มากที่สุด) จนกว่าหน่วยความจำบัฟเฟอร์เติมแล้ววิ่งอ่านข้อมูลจากบัฟเฟอร์ (มากที่สุด) จนกว่าจะมีที่ว่างเปล่า เช่นและทำงานในเวลาเดียวกันไม่จำเป็นต้องมีข้อมูลจำนวน จำกัด "ระหว่างเที่ยวบิน" ระหว่างกัน หากคุณต้องการเชื่อมต่ออินพุตหลายอินพุทกับเอาท์พุทเดียวหากผู้อ่านคนใดคนหนึ่งล่าช้าหลังคนอื่นไม่ว่าคุณจะหยุดสิ่งอื่น (จุดของการทำงานในแบบคู่ขนานนั้นคืออะไร?) หรือคุณเอาออกไป (จุดประสงค์ของการไม่มีไฟล์ระดับกลางคืออะไร?)cmd1 | cmd2cmd1cmd2cmd1cmd2 ซับซ้อนยิ่งขึ้น.

ในเกือบ 30 ปีของประสบการณ์ Unix ฉันจำไม่ได้ว่ามีสถานการณ์ใดที่จะได้รับประโยชน์จากการใช้งานท่อส่งออกที่หลากหลาย

คุณสามารถรวมเอาท์พุทหลาย ๆ อย่างไว้ในหนึ่งสตรีมได้แล้ววันนี้โดยไม่ต้องใช้วิธีอินเตอร์เลซ (วิธีที่เอ้าท์พุทของcmd1และcmd2อินเตอร์เลเตอร์นั้นควรทำอย่างไร? ไม่ต้องเขียนอะไรเลยเป็นเวลานานทั้งหมดนี้ซับซ้อนในการจัดการ) จะทำโดยการเช่น(cmd1; cmd2; cmd3) | cmd4โปรแกรมcmd1, cmd2และcmd3จะดำเนินการอย่างใดอย่างหนึ่งหลังจากที่อื่น ๆ ที่ส่งออกจะถูกส่งเป็น input cmd4เพื่อ


3

สำหรับปัญหาการซ้อนทับกันของคุณบน Linux (และด้วยbashหรือzshไม่ใช้ksh93) คุณสามารถทำได้ดังนี้:

somefunction()
(
  if [ "$1" -eq 1 ]
  then
    echo "Hello world!"
  else
    exec 3> auxfile
    rm -f auxfile
    somefunction "$(($1 - 1))" >&3 auxfile 3>&-
    exec cat <(command1 < /dev/fd/3) \
             <(command2 < /dev/fd/3) \
             <(command3 < /dev/fd/3)
  fi
)

หมายเหตุการใช้ของ(...)แทนการ{...}ที่จะได้รับกระบวนการใหม่ที่ซ้ำกันเพื่อให้เราสามารถมีใหม่ FD 3 auxfileชี้ไปที่ใหม่ < /dev/fd/3เป็นเคล็ดลับในการเข้าถึงไฟล์ที่ถูกลบตอนนี้ มันจะไม่ทำงานบนระบบอื่น ๆ นอกเหนือจากลีนุกซ์ที่< /dev/fd/3มีลักษณะเป็นเช่นdup2(3, 0)นั้นและ fd 0 จะเปิดในโหมดเขียนอย่างเดียวพร้อมเคอร์เซอร์ที่ท้ายไฟล์

เพื่อหลีกเลี่ยงทางแยกสำหรับฟังก์ชั่นที่ซ้อนกันคุณสามารถเขียนเป็น:

somefunction()
{
  if [ "$1" -eq 1 ]
  then
    echo "Hello world!"
  else
    {
      rm -f auxfile
      somefunction "$(($1 - 1))" >&3 auxfile 3>&-
      exec cat <(command1 < /dev/fd/3) \
               <(command2 < /dev/fd/3) \
               <(command3 < /dev/fd/3)
    } 3> auxfile
  fi
}

เชลล์จะดูแลการสำรอง fd 3 ในแต่ละการวนซ้ำ คุณจะพบว่าตัวเขียนไฟล์หมดเร็วกว่านั้น

แม้ว่าคุณจะพบว่ามันมีประสิทธิภาพมากกว่าในการทำสิ่งต่อไปนี้:

somefunction() {
  if [ "$1" -eq 1 ]; then
    echo "Hello world!" > auxfile
  else
    somefunction "$(($1 - 1))"
    { rm -f auxfile
      cat <(command1 < /dev/fd/3) \
          <(command2 < /dev/fd/3) \
          <(command3 < /dev/fd/3) > auxfile
    } 3< auxfile
  fi
}
somefunction 12; cat auxfile

นั่นคือไม่ซ้อนการเปลี่ยนเส้นทาง

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