2017/06/29


今日とある場所で虫が入り込む瞬間を見た。虫といってもバグの方。それはプログラマ向けの Q&A サイトで始まった。質問の内容はこうだ。

文字列には0または4がだけが含まれる。文字列は 4 から始まり、例えば 440, 44, 40, 4400, 4440 など、これらは正しいとするが 404 は正しくない。今のところ、私は 0 の直後に 4 が現れるかどうかでチェックしている。これは果たして効率的だろうか。

始め僕はこの質問文をちゃんと読んでおらず、正規表現を使ってこれを実装した。

package main

import (
    "regexp"
)

func check(s stringbool {
    return regexp.MustCompile(`^4+0*$`).MatchString(s)
}

func main() {
    for _, tt := range []string{"444""44""40""4400""4440"} {
        if !check(tt) {
            panic("want true: " + tt)
        }
    }
    for _, tt := range []string{"404""040"} {
        if check(tt) {
            panic("want false: " + tt)
        }
    }
}

でも質問をよく見たら彼は効率的かどうかを気にしていた。確かにこのお題で正規表現は無い。僕は慌てて以下のコードを付け添えた。

package main

func check(s stringbool {
    i := 0
    r := []rune(s)
    for i = 0; i < len(r); i++ {
        if r[i] != '4' {
            break
        }
    }
    if i == 0 {
        return false
    }
    for ; i < len(r); i++ {
        if r[i] != '0' {
            return false
        }
    }
    return true
}

func main() {
    for _, tt := range []string{"444""44""40""4400""4440"} {
        if !check(tt) {
            panic("want true: " + tt)
        }
    }
    for _, tt := range []string{"404""040"} {
        if check(tt) {
            panic("want false: " + tt)
        }
    }
}

いずれのパッケージにも依存しておらく、おそらくちゃんと動くコードだろう。

その後、周りのオーディエンスが質問した彼に「どんなケースか良く分からないな、コードを見せてくれる?」と言った。そして彼は以下のコードを見せてくれた。

package main

import (
    "fmt"
    "strings"
)

func validate(str stringbool {

    if strings.HasPrefix(str, "4") {
        for i := 0; i < len(str)-1; i++ {
            if (str[i] == '0'&& (str[i+1== '4') {
                return false
            }
        }

    } else {
        return false
    }

    return true
}

func main() {

    data := []string{"4""44""4400""4440""404""004"}
    for _, val := range data {
        fmt.Println(validate(val))
    }
}

なるほど彼が最初に言ってた通り「0 の直後に 4 が現れる事でチェック」している。一見このコードは正しそうに見える。でもこのコードは「406」の様な文字列で正しく機能しない。彼にそれを伝えたところ、彼は「@mattn -1 最初に 0 と 4 しか無いっていったじゃん」と返してきた。

僕はここで「あー、バグが混入するタイミングはここなんだ」と思った。例えばこの関数が「4 から始まり 0 が 0 個以上続く文字列をチェックする」関数だとして他のユーザに配られるとする。それを譲り受けた開発者はそれを使ってテストする。この時点でその開発者は「もちろん 406 みたいな文字列も弾いてくれる」と信じてしまうだろう。こうやってバグってのは混入するんだ、と思った。まぁ、もしかしたら彼のキーボードには 0 と 4 とリターンキーしか付いていなかったのかもしれない。

Posted at by



2017/06/22


要求仕様から工数を出す側から言うと「ブラウザのダウンロード画面に進捗出てるから要らないでしょ」と言いたい所でしたが「出来ないのか」と言われると「出来るもん」と言わざると得ないエンジニア魂。

JavaScript - ブラウザから、ファイルをダウンロードしている途中で、プログレスバーを実装したい。完了したら、プログレスバーを閉じたい。(81363)|teratail

前提・実現したいこと javaScript/HTML/CSSを利用しております。 目的は、ブラウザから、ファイルをダウンロードしている途中で、プログレスバーを実装したい。完了したら、プログレスバーを閉...

https://teratail.com/questions/81363

通常、ブラウザからファイルをダウンロードする際は javascript からは制御できません。サーバからバイト列を JSON で Range っぽく返して最後に data スキームでダウンロードダイアログを出す、といったニッチなテクニックでも事も出来なくないですがブラウザにメモリを保持してしまって大きいファイルだとハングしかねない等の問題が発生します。で、どうやるかというとまずはサーバの処理

package main

import (
    "fmt"
    "log"
    "net/http"
    "os"
    "sync"

    "github.com/google/uuid"
    "github.com/labstack/echo"
)

var (
    m = &sync.Map{}
)

// ダウンロードの進捗を JSON で返す
func stat(c echo.Context) error {
    ck, err := c.Cookie("download-progress")
    if err != nil {
        log.Println(err)
        return err
    }
    progress := 0
    v, ok := m.Load(ck.Value)
    if ok {
        if vi, ok := v.(int); ok {
            progress = vi
        }
    }
    return c.JSON(http.StatusOK, &struct {
        Progress int `json:"progress"`
    }{
        Progress: progress,
    })
}

// クライアントにデータを送信しつつ進捗を更新
func download(c echo.Context) error {
    id := uuid.New().String()
    c.SetCookie(&http.Cookie{
        Name:  "download-progress",
        Value: id,
    })
    f, err := os.Open("ubuntu-17.04-server-amd64.iso")
    if err != nil {
        log.Println(err)
        return err
    }
    defer f.Close()
    st, err := f.Stat()
    if err != nil {
        log.Println(err)
        return err
    }
    total := st.Size()
    rest := total
    m.Store(id, 0)

    w := c.Response().Writer
    w.Header().Set("Content-Disposition""attachment")
    w.Header().Set("Content-Length", fmt.Sprint(total))
    for {
        var b [4098]byte
        n, err := f.Read(b[:])
        if err != nil {
            break
        }
        _, err = w.Write(b[:n])
        if err != nil {
            break
        }
        rest -= int64(n)
        m.Store(id, int((total-rest)*100/total))
        if total <= 0 {
            break
        }
    }
    m.Store(id, nil)
    return nil
}

func main() {
    e := echo.New()

    e.GET("/stat", stat)
    e.GET("/download", download)

    e.Static("/""static")
    e.Logger.Fatal(e.Start(":8989"))
}

そしてクライアント側の処理

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>download</title>
<script>
window.addEventListener('load'function() {
  function progress() {
    fetch("/stat"{
      'credentials'"same-origin"
    }).then(function(response) {
      return response.json();
    }).then(function(json) {
      document.querySelector('#progress').textContent = json.progress + "%";
      if (json.progress < 100) setTimeout(progress, 1000);
    })
  }
  document.querySelector('#download').addEventListener('click'function() {
    progress();
    return true;
  });
}false);
</script>
</head>
<body>
    <p>
        <span id="progress"></span>
    </p>
    <a id="download" href="/download">Download</a>
</body>
</html>

ダウンロードが始まったらランダムIDでクッキーを返送し、そのIDでステータスの要求を受け付ける。ダウンロードは細かい単位で行い都度進捗を更新する。こうすればダウンロードが始まれば進捗がパーセンテージで表示され、終了すればタイマーが止まる。プログレスバー表示やダウンロードをキャンセルした際の処理はめんどくさいので実装してないですが分かりますよね。あとダウンロードが終わったら m から破棄しないと何時かサーバがパンクしますよっと。

ダウンロードの進捗表示は出来なくはない。ただ、これだけは言っておきたい。

実装は、仕事でやるならタダじゃない(575)
Posted at by



2017/06/14


Windows で hosts ファイル(C:\Windows\System32\drivers\etc\hosts) を編集するには管理者権限が必要です。またコマンドラインから IP アドレスを変更するのにも管理者権限が必要です。管理者権限で hosts ファイルを編集するにはメモ帳を管理者権限で起動する必要があります。管理者権限でメモ帳を起動する為にはメニューから「メモ帳」を出し、右クリックして「管理者として実行」を選ぶ必要があります。そして実行したメモ帳のメニューから「開く」でファイルを選択します。管理者として開かないのであれば hosts ファイルを右クリックして「送る」等に登録したエディタを単に選べば済む話なのに、随分と手間ですね。

notepad

UNIX だと

$ sudo vi /etc/hosts

とだけタイプすれば良いのにこの手数の多さはちょっとゲンナリします。特にマウスに手を伸ばしたくないからコマンドプロンプトを使っている僕の様な変な人には辛さしかありません。runas というコマンドを使えば sudo の様な事は出来るのですが、これはパスワード入力を要求されます。メモ帳を管理者として起動する場合だと UAC (User Account Control) のダイアログが表示されるだけなのに、いちいちパスワードなんか打ちたくありません。世の中には C言語で実装した物、タスクスケジューラを使って sudo ぽい事する物、powershell を使って実現している物、いろいろありますがそれぞれ難があるし出来ればキビキビ動くのが欲しかったので作りました。

GitHub - mattn/sudo: sudo for windows

README.md sudo for windows Usage C:\>sudo cmd /c dir Then, you'll see the UAC dialog. Tutorials Disp...

https://github.com/mattn/sudo

標準入出力を扱えるので

sudo cmd /c type secret-file.txt > accessible-file.txt

リダイレクトしたり

echo 123 | sudo my-command.exe | more

パイプで繋げたり出来ます。コマンドプロンプト内のコマンド(例: typeecho)は cmd /c echo の様に起動する必要があります。普段は管理者権限が無いと実行出来ない netsh による IP アドレスの変更も簡単。

sudo netsh interface ip add address "ローカルネットワーク" 33.33.33.33 255.255.255.255

やりたかったメモ帳での hosts ファイル編集も

sudo notepad c:\windows\system32\drivers\etc\hosts
と簡単に出来る様になりました。これを実現する為に、ShellExecuteEx という API の Verb 指定に runas を付けて起動するという方法を取ったのですが、この API で起動したプロセスは、それを起動した同じコンソールを共有する事が出来なかったのでコマンドプロンプトから sudo vim を実行できる様にする事は諦めました。一応、sudo -spawn vim foo.txt で新しいコマンドプロンプトを起動するという機能を付けてありますので、どうしても vim じゃないと嫌だという方はそちらをお使い下さい。間違って起動してしまった場合も CTRL-C するとプロセスを終了する仕組みが入っています。NTサービスをコマンドプロンプトから操作する事も出来るのでずいぶんと楽になりました。
ntservice

Windows でコマンドプロンプトで生活してて sudo したいという、かなり絞られたユーザ層に届けばいいなと思います。

Posted at by