<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Go每日一庫(kù)之調(diào)用外部命令的幾種姿勢(shì)

          共 11178字,需瀏覽 23分鐘

           ·

          2022-11-05 06:17

          引子

          在工作中,我時(shí)不時(shí)地會(huì)需要在Go中調(diào)用外部命令。前段時(shí)間我做了一個(gè)工具,在釘釘群中添加了一個(gè)機(jī)器人,@這個(gè)機(jī)器人可以讓它執(zhí)行一些寫(xiě)好的腳本程序完成指定的任務(wù)。機(jī)器人倒是不難,照著釘釘開(kāi)發(fā)者文檔添加好機(jī)器人,然后@這個(gè)機(jī)器人就會(huì)向一個(gè)你指定的服務(wù)器發(fā)送一個(gè)POST請(qǐng)求,請(qǐng)求中會(huì)附帶文本消息。所以我要做的就是搭一個(gè)Web服務(wù)器,可以用go原生的net/http包,也可以用gin/fasthttp/fiber這些Web框架。收到請(qǐng)求之后,檢查附帶文本中的關(guān)鍵字去調(diào)用對(duì)應(yīng)的程序,然后返回結(jié)果。

          go標(biāo)準(zhǔn)庫(kù)中的os/exec包對(duì)調(diào)用外部程序提供了支持,本文詳細(xì)介紹os/exec的使用姿勢(shì)。

          運(yùn)行命令

          Linux中有個(gè)cal命令,它可以顯示指定年、月的日歷,如果不指定年、月,默認(rèn)為當(dāng)前時(shí)間對(duì)應(yīng)的年月。如果使用的是Windows,推薦安裝msys2,這個(gè)軟件包含了絕大多數(shù)的Linux常用命令。

          5188b510f7ace7b0f45f1c8cc146b11a.webp


          5871ab6e313e94d54fc59db0ffcec9d5.webp

          那么,在Go代碼中怎么調(diào)用這個(gè)命令呢?其實(shí)也很簡(jiǎn)單:

                
                func?main()?{
          ??cmd?:=?exec.Command("cal")
          ??err?:=?cmd.Run()
          ??if?err?!=?nil?{
          ????log.Fatalf("cmd.Run()?failed:?%v\n",?err)
          ??}
          }

          首先,我們調(diào)用exec.Command傳入命令名,創(chuàng)建一個(gè)命令對(duì)象exec.Cmd。接著調(diào)用該命令對(duì)象的Run()方法運(yùn)行它。

          如果你實(shí)際運(yùn)行了,你會(huì)發(fā)現(xiàn)什么也沒(méi)有發(fā)生,哈哈。事實(shí)上,使用os/exec執(zhí)行命令,標(biāo)準(zhǔn)輸出和標(biāo)準(zhǔn)錯(cuò)誤默認(rèn)會(huì)被丟棄。

          顯示輸出

          exec.Cmd對(duì)象有兩個(gè)字段StdoutStderr,類(lèi)型皆為io.Writer。我們可以將任意實(shí)現(xiàn)了io.Writer接口的類(lèi)型實(shí)例賦給這兩個(gè)字段,繼而實(shí)現(xiàn)標(biāo)準(zhǔn)輸出和標(biāo)準(zhǔn)錯(cuò)誤的重定向。io.Writer接口在 Go 標(biāo)準(zhǔn)庫(kù)和第三方庫(kù)中隨處可見(jiàn),例如*os.File*bytes.Buffer、net.Conn。所以我們可以將命令的輸出重定向到文件、內(nèi)存緩存甚至發(fā)送到網(wǎng)絡(luò)中。

          顯示到標(biāo)準(zhǔn)輸出

          exec.Cmd對(duì)象的StdoutStderr這兩個(gè)字段都設(shè)置為os.Stdout,那么輸出內(nèi)容都將顯示到標(biāo)準(zhǔn)輸出:

                
                func?main()?{
          ??cmd?:=?exec.Command("cal")
          ??cmd.Stdout?=?os.Stdout
          ??cmd.Stderr?=?os.Stderr
          ??err?:=?cmd.Run()
          ??if?err?!=?nil?{
          ????log.Fatalf("cmd.Run()?failed:?%v\n",?err)
          ??}
          }

          運(yùn)行程序。我在git bash運(yùn)行,得到如下結(jié)果:

          ce8358dd26afdc328506b9568e26fbd2.webp

          輸出了中文,檢查一下環(huán)境變量LANG的值,果然是zh_CN.UTF-8。如果想輸出英文,可以將環(huán)境變量LANG設(shè)置為en_US.UTF-8

                
                $?echo?$LANG
          zh_CN.UTF-8
          $?LANG=en_US.UTF-8?go?run?main.go

          得到輸出:

          c013fcffc789b799a9a92f77e88258a4.webp

          輸出到文件

          打開(kāi)或創(chuàng)建文件,然后將文件句柄賦給exec.Cmd對(duì)象的StdoutStderr這兩個(gè)字段即可實(shí)現(xiàn)輸出到文件的功能。

                
                func?main()?{
          ??f,?err?:=?os.OpenFile("out.txt",?os.O_WRONLY|os.O_CREATE,?os.ModePerm)
          ??if?err?!=?nil?{
          ????log.Fatalf("os.OpenFile()?failed:?%v\n",?err)
          ??}

          ??cmd?:=?exec.Command("cal")
          ??cmd.Stdout?=?f
          ??cmd.Stderr?=?f
          ??err?=?cmd.Run()
          ??if?err?!=?nil?{
          ????log.Fatalf("cmd.Run()?failed:?%v\n",?err)
          ??}
          }

          os.OpenFile打開(kāi)一個(gè)文件,指定os.O_CREATE標(biāo)志讓操作系統(tǒng)在文件不存在時(shí)自動(dòng)創(chuàng)建一個(gè),返回該文件對(duì)象*os.File。*os.File實(shí)現(xiàn)了io.Writer接口。

          運(yùn)行程序:

                
                $?go?run?main.go
          $?cat?out.txt
          ????November?2022???
          Su?Mo?Tu?We?Th?Fr?Sa
          ???????1??2??3??4??5
          ?6??7??8??9?10?11?12
          13?14?15?16?17?18?19
          20?21?22?23?24?25?26
          27?28?29?30

          發(fā)送到網(wǎng)絡(luò)

          現(xiàn)在我們來(lái)編寫(xiě)一個(gè)日歷服務(wù),接收年、月信息,返回該月的日歷。

                
                func?cal(w?http.ResponseWriter,?r?*http.Request)?{
          ??year?:=?r.URL.Query().Get("year")
          ??month?:=?r.URL.Query().Get("month")

          ??cmd?:=?exec.Command("cal",?month,?year)
          ??cmd.Stdout?=?w
          ??cmd.Stderr?=?w

          ??err?:=?cmd.Run()
          ??if?err?!=?nil?{
          ????log.Fatalf("cmd.Run()?failed:?%v\n",?err)
          ??}
          }

          func?main()?{
          ??http.HandleFunc("/cal",?cal)
          ??http.ListenAndServe(":8080",?nil)
          }

          這里為了簡(jiǎn)單,錯(cuò)誤處理都省略了。正常情況下,year和month參數(shù)都需要做合法性校驗(yàn)。exec.Command函數(shù)接收一個(gè)字符串類(lèi)型的可變參數(shù)作為命令的參數(shù):

                
                
                  func?Command(name?string,?arg?...string)?*Cmd
                  

          運(yùn)行程序,使用瀏覽器請(qǐng)求localhost:8080/cal?year=2021&month=2得到:

          dda6ffe90dd0121d6e5bcf916db49776.webp

          保存到內(nèi)存對(duì)象中

          *bytes.Buffer同樣也實(shí)現(xiàn)了io.Writer接口,故如果我們創(chuàng)建一個(gè)*bytes.Buffer對(duì)象,并將其賦給exec.CmdStdoutStderr這兩個(gè)字段,那么命令執(zhí)行之后,該*bytes.Buffer對(duì)象中保存的就是命令的輸出。

                
                func?main()?{
          ??buf?:=?bytes.NewBuffer(nil)
          ??cmd?:=?exec.Command("cal")
          ??cmd.Stdout?=?buf
          ??cmd.Stderr?=?buf
          ??err?:=?cmd.Run()
          ??if?err?!=?nil?{
          ????log.Fatalf("cmd.Run()?failed:?%v\n",?err)
          ??}

          ??fmt.Println(buf.String())
          }

          運(yùn)行:

                
                $?go?run?main.go
          ????November?2022???
          Su?Mo?Tu?We?Th?Fr?Sa
          ???????1??2??3??4??5
          ?6??7??8??9?10?11?12
          13?14?15?16?17?18?19
          20?21?22?23?24?25?26
          27?28?29?30

          運(yùn)行命令,然后得到輸出的字符串或字節(jié)切片這種模式是如此的普遍,并且使用便利,os/exec包提供了一個(gè)便捷方法:CombinedOutput

          輸出到多個(gè)目的地

          有時(shí),我們希望能輸出到文件和網(wǎng)絡(luò),同時(shí)保存到內(nèi)存對(duì)象。使用go提供的io.MultiWriter可以很容易實(shí)現(xiàn)這個(gè)需求。io.MultiWriter很方便地將多個(gè)io.Writer轉(zhuǎn)為一個(gè)io.Writer。

          我們稍微修改上面的web程序:

                
                func?cal(w?http.ResponseWriter,?r?*http.Request)?{
          ??year?:=?r.URL.Query().Get("year")
          ??month?:=?r.URL.Query().Get("month")

          ??f,?_?:=?os.OpenFile("out.txt",?os.O_CREATE|os.O_WRONLY,?os.ModePerm)
          ??buf?:=?bytes.NewBuffer(nil)
          ??mw?:=?io.MultiWriter(w,?f,?buf)

          ??cmd?:=?exec.Command("cal",?month,?year)
          ??cmd.Stdout?=?mw
          ??cmd.Stderr?=?mw

          ??err?:=?cmd.Run()
          ??if?err?!=?nil?{
          ????log.Fatalf("cmd.Run()?failed:?%v\n",?err)
          ??}

          ??fmt.Println(buf.String())
          }

          調(diào)用io.MultiWriter將多個(gè)io.Writer整合成一個(gè)io.Writer,然后將cmd對(duì)象的StdoutStderr都賦值為這個(gè)io.Writer。這樣,命令運(yùn)行時(shí)產(chǎn)出的輸出會(huì)分別送往http.ResponseWriter*os.File以及*bytes.Buffer。

          運(yùn)行命令,獲取輸出

          前面提到,我們常常需要運(yùn)行命令,返回輸出。exec.Cmd對(duì)象提供了一個(gè)便捷方法:CombinedOutput()。該方法運(yùn)行命令,將輸出內(nèi)容以一個(gè)字節(jié)切片返回便于后續(xù)處理。所以,上面獲取輸出的程序可以簡(jiǎn)化為:

                
                func?main()?{
          ??cmd?:=?exec.Command("cal")
          ??output,?err?:=?cmd.CombinedOutput()
          ??if?err?!=?nil?{
          ????log.Fatalf("cmd.Run()?failed:?%v\n",?err)
          ??}

          ??fmt.Println(string(output))
          }

          So easy!

          CombinedOutput()方法的實(shí)現(xiàn)很簡(jiǎn)單,先將標(biāo)準(zhǔn)輸出和標(biāo)準(zhǔn)錯(cuò)誤重定向到*bytes.Buffer對(duì)象,然后運(yùn)行程序,最后返回該對(duì)象中的字節(jié)切片:

                
                func?(c?*Cmd)?CombinedOutput()?([]byte,?error)?{
          ??if?c.Stdout?!=?nil?{
          ????return?nil,?errors.New("exec:?Stdout?already?set")
          ??}
          ??if?c.Stderr?!=?nil?{
          ????return?nil,?errors.New("exec:?Stderr?already?set")
          ??}
          ??var?b?bytes.Buffer
          ??c.Stdout?=?&b
          ??c.Stderr?=?&b
          ??err?:=?c.Run()
          ??return?b.Bytes(),?err
          }

          CombinedOutput方法前幾行判斷表明,StdoutStderr必須是未設(shè)置狀態(tài)。這其實(shí)很好理解,一般情況下,如果已經(jīng)打算使用CombinedOutput方法獲取輸出內(nèi)容,不會(huì)再自找麻煩地再去設(shè)置StdoutStderr字段了。

          CombinedOutput類(lèi)似的還有Output方法,區(qū)別是Output只會(huì)返回運(yùn)行命令產(chǎn)出的標(biāo)準(zhǔn)輸出內(nèi)容。

          分別獲取標(biāo)準(zhǔn)輸出和標(biāo)準(zhǔn)錯(cuò)誤

          創(chuàng)建兩個(gè)*bytes.Buffer對(duì)象,分別賦給exec.Cmd對(duì)象的StdoutStderr這兩個(gè)字段,然后運(yùn)行命令即可分別獲取標(biāo)準(zhǔn)輸出和標(biāo)準(zhǔn)錯(cuò)誤。

                
                func?main()?{
          ??cmd?:=?exec.Command("cal",?"15",?"2012")
          ??var?stdout,?stderr?bytes.Buffer
          ??cmd.Stdout?=?&stdout
          ??cmd.Stderr?=?&stderr
          ??err?:=?cmd.Run()
          ??if?err?!=?nil?{
          ????log.Fatalf("cmd.Run()?failed:?%v\n",?err)
          ??}

          ??fmt.Printf("output:\n%s\nerror:\n%s\n",?stdout.String(),?stderr.String())
          }

          標(biāo)準(zhǔn)輸入

          exec.Cmd對(duì)象有一個(gè)類(lèi)型為io.Reader的字段Stdin。命令運(yùn)行時(shí)會(huì)從這個(gè)io.Reader讀取輸入。先來(lái)看一個(gè)最簡(jiǎn)單的例子:

                
                func?main()?{
          ??cmd?:=?exec.Command("cat")
          ??cmd.Stdin?=?bytes.NewBufferString("hello\nworld")
          ??cmd.Stdout?=?os.Stdout
          ??err?:=?cmd.Run()
          ??if?err?!=?nil?{
          ????log.Fatalf("cmd.Run()?failed:?%v\n",?err)
          ??}
          }

          如果不帶參數(shù)運(yùn)行cat命令,則進(jìn)入交互模式,cat按行讀取輸入,并且原樣發(fā)送到輸出。

          再來(lái)看一個(gè)復(fù)雜點(diǎn)的例子。Go標(biāo)準(zhǔn)庫(kù)中compress/bzip2包只提供解壓方法,并沒(méi)有壓縮方法。我們可以利用Linux命令bzip2實(shí)現(xiàn)壓縮。bzip2從標(biāo)準(zhǔn)輸入中讀取數(shù)據(jù),將其壓縮,并發(fā)送到標(biāo)準(zhǔn)輸出。

                
                func?bzipCompress(d?[]byte)?([]byte,?error)?{
          ??var?out?bytes.Buffer
          ??cmd?:=?exec.Command("bzip2",?"-c",?"-9")
          ??cmd.Stdin?=?bytes.NewBuffer(d)
          ??cmd.Stdout?=?&out
          ??err?:=?cmd.Run()
          ??if?err?!=?nil?{
          ????log.Fatalf("cmd.Run()?failed:?%v\n",?err)
          ??}

          ??return?out.Bytes(),?nil
          }

          參數(shù)-c表示壓縮,-9表示壓縮等級(jí),9為最高。為了驗(yàn)證函數(shù)的正確性,寫(xiě)個(gè)簡(jiǎn)單的程序,先壓縮"hello world"字符串,然后解壓,看看是否能得到原來(lái)的字符串:

                
                func?main()?{
          ??data?:=?[]byte("hello?world")
          ??compressed,?_?:=?bzipCompress(data)
          ??r?:=?bzip2.NewReader(bytes.NewBuffer(compressed))
          ??decompressed,?_?:=?ioutil.ReadAll(r)
          ??fmt.Println(string(decompressed))
          }

          運(yùn)行程序,輸出"hello world"。

          環(huán)境變量

          環(huán)境變量可以在一定程度上微調(diào)程序的行為,當(dāng)然這需要程序的支持。例如,設(shè)置ENV=production會(huì)抑制調(diào)試日志的輸出。每個(gè)環(huán)境變量都是一個(gè)鍵值對(duì)。exec.Cmd對(duì)象中有一個(gè)類(lèi)型為[]string的字段Env。我們可以通過(guò)修改它來(lái)達(dá)到控制命令運(yùn)行時(shí)的環(huán)境變量的目的。

                
                package?main

          import?(
          ??"fmt"
          ??"log"
          ??"os"
          ??"os/exec"
          )

          func?main()?{
          ??cmd?:=?exec.Command("bash",?"-c",?"./test.sh")

          ??nameEnv?:=?"NAME=darjun"
          ??ageEnv?:=?"AGE=18"

          ??newEnv?:=?append(os.Environ(),?nameEnv,?ageEnv)
          ??cmd.Env?=?newEnv

          ??out,?err?:=?cmd.CombinedOutput()
          ??if?err?!=?nil?{
          ????log.Fatalf("cmd.Run()?failed:?%v\n",?err)
          ??}

          ??fmt.Println(string(out))
          }

          上面代碼獲取系統(tǒng)的環(huán)境變量,然后又添加了兩個(gè)環(huán)境變量NAMEAGE。最后使用bash運(yùn)行腳本test.sh

                
                #!/bin/bash

          echo?$NAME
          echo?$AGE
          echo?$GOPATH

          程序運(yùn)行結(jié)果:

                
                $?go?run?main.go?
          darjun
          18
          D:\workspace\code\go

          檢查命令是否存在

          一般在運(yùn)行命令之前,我們通過(guò)希望能檢查要運(yùn)行的命令是否存在,如果存在則直接運(yùn)行,否則提示用戶(hù)安裝此命令。os/exec包提供了函數(shù)LookPath可以獲取命令所在目錄,如果命令不存在,則返回一個(gè)error。

                
                func?main()?{
          ??path,?err?:=?exec.LookPath("ls")
          ??if?err?!=?nil?{
          ????fmt.Printf("no?cmd?ls:?%v\n",?err)
          ??}?else?{
          ????fmt.Printf("find?ls?in?path:%s\n",?path)
          ??}

          ??path,?err?=?exec.LookPath("not-exist")
          ??if?err?!=?nil?{
          ????fmt.Printf("no?cmd?not-exist:?%v\n",?err)
          ??}?else?{
          ????fmt.Printf("find?not-exist?in?path:%s\n",?path)
          ??}
          }

          運(yùn)行:

                
                $?go?run?main.go?
          find?ls?in?path:C:\Program?Files\Git\usr\bin\ls.exe
          no?cmd?not-exist:?exec:?"not-exist":?executable?file?not?found?in?%PATH%

          封裝

          執(zhí)行外部命令的流程比較固定:

          • 調(diào)用exec.Command()創(chuàng)建命令對(duì)象;
          • 調(diào)用Cmd.Run()執(zhí)行命令

          如果要獲取輸出,需要調(diào)用CombinedOutput/Output之類(lèi)的方法,或者手動(dòng)創(chuàng)建bytes.Buffer對(duì)象并賦值給exec.CmdStdoutStderr字段。為了使用方便,我編寫(xiě)了一個(gè)包goexec

          接口如下:

                
                
                  //?執(zhí)行命令,丟棄標(biāo)準(zhǔn)輸出和標(biāo)準(zhǔn)錯(cuò)誤
                  
          func?RunCommand(cmd?string,?arg?[]string,?opts?...Option)?error
          //?執(zhí)行命令,以[]byte類(lèi)型返回輸出
          func?CombinedOutput(cmd?string,?arg?[]string,?opts?...Option)?([]byte,?error)
          //?執(zhí)行命令,以string類(lèi)型返回輸出
          func?CombinedOutputString(cmd?string,?arg?[]string,?opts?...Option)?(string,?error)
          //?執(zhí)行命令,以[]byte類(lèi)型返回標(biāo)準(zhǔn)輸出
          func?Output(cmd?string,?arg?[]string,?opts?...Option)?([]byte,?error)
          //?執(zhí)行命令,以string類(lèi)型返回標(biāo)準(zhǔn)輸出
          func?OutputString(cmd?string,?arg?[]string,?opts?...Option)?(string,?error)
          //?執(zhí)行命令,以[]byte類(lèi)型分別返回標(biāo)準(zhǔn)輸出和標(biāo)準(zhǔn)錯(cuò)誤
          func?SeparateOutput(cmd?string,?arg?[]string,?opts?...Option)?([]byte,?[]byte,?error)
          //?執(zhí)行命令,以string類(lèi)型分別返回標(biāo)準(zhǔn)輸出和標(biāo)準(zhǔn)錯(cuò)誤
          func?SeparateOutputString(cmd?string,?arg?[]string,?opts?...Option)?(string,?string,?error)

          相較于直接使用os/exec包,我傾向于一次函數(shù)調(diào)用就能獲得結(jié)果。對(duì)輸入、設(shè)置環(huán)境變量這些功能,我通過(guò)Option模式來(lái)提供支持。

                
                type?Option?func(*exec.Cmd)

          func?WithStdin(stdin?io.Reader)?Option?{
          ??return?func(c?*exec.Cmd)?{
          ????c.Stdin?=?stdin
          ??}
          }

          func?Without(stdout?io.Writer)?Option?{
          ??return?func(c?*exec.Cmd)?{
          ????c.Stdout?=?stdout
          ??}
          }

          func?WithStderr(stderr?io.Writer)?Option?{
          ??return?func(c?*exec.Cmd)?{
          ????c.Stderr?=?stderr
          ??}
          }

          func?WithOutWriter(out?io.Writer)?Option?{
          ??return?func(c?*exec.Cmd)?{
          ????c.Stdout?=?out
          ????c.Stderr?=?out
          ??}
          }

          func?WithEnv(key,?value?string)?Option?{
          ??return?func(c?*exec.Cmd)?{
          ????c.Env?=?append(os.Environ(),?fmt.Sprintf("%s=%s",?key,?value))
          ??}
          }

          func?applyOptions(cmd?*exec.Cmd,?opts?[]Option)?{
          ??for?_,?opt?:=?range?opts?{
          ????opt(cmd)
          ??}
          }

          使用非常簡(jiǎn)單:

                
                func?main()?{
          ??fmt.Println(goexec.CombinedOutputString("cal",?nil,?goexec.WithEnv("LANG",?"en_US.UTF-8")))
          }

          有一點(diǎn)我不太滿(mǎn)意,為了使用Option模式,本來(lái)可以用可變參數(shù)來(lái)傳遞命令參數(shù),現(xiàn)在只能用切片了,即使不需要指定參數(shù),也必須要傳入一個(gè)nil。暫時(shí)還沒(méi)有想到比較優(yōu)雅的解決方法。

          總結(jié)

          本文介紹了使用os/exec這個(gè)標(biāo)準(zhǔn)庫(kù)調(diào)用外部命令的各種姿勢(shì)。同時(shí)為了便于使用,我編寫(xiě)了一個(gè)goexec包封裝對(duì)os/exec的調(diào)用。這個(gè)包目前for我自己使用是沒(méi)有問(wèn)題的,大家有其他需求可以提issue或者自己魔改??。

          大家如果發(fā)現(xiàn)好玩、好用的 Go 語(yǔ)言庫(kù),歡迎到 Go 每日一庫(kù) GitHub 上提交 issue??

          參考

          1. Advanced command execution in go with os/exec: https://blog.kowalczyk.info/article/wOYk/advanced-command-execution-in-go-with-osexec.html
          2. goexec: https://github.com/darjun/goexec
          3. Go 每日一庫(kù) GitHub:https://github.com/darjun/go-daily-lib


          推薦閱讀


          福利
          我為大家整理了一份 從入門(mén)到進(jìn)階的Go學(xué)習(xí)資料禮包 ,包含學(xué)習(xí)建議:入門(mén)看什么,進(jìn)階看什么。 關(guān)注公眾號(hào) 「polarisxu」,回復(fù)? ebook ?獲??;還可以回復(fù)「進(jìn)群」,和數(shù)萬(wàn) Gopher 交流學(xué)習(xí)。

          瀏覽 43
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产精品无码污污污 | 黄片在线免费看在线 | 免费A√在线播放 | 中文字幕在线无码视频 | 丁香五月激情中文字幕 |