Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(90)

Side by Side Diff: experimental/webtry/webtry.go

Issue 240773003: First pass at workspaces. (Closed) Base URL: https://skia.googlesource.com/skia.git@master
Patch Set: clean up sql in design doc. Created 6 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 package main 1 package main
2 2
3 import ( 3 import (
4 "bytes" 4 "bytes"
5 "crypto/md5" 5 "crypto/md5"
6 "database/sql" 6 "database/sql"
7 "encoding/base64" 7 "encoding/base64"
8 "encoding/json" 8 "encoding/json"
9 "flag" 9 "flag"
10 "fmt" 10 "fmt"
11 _ "github.com/go-sql-driver/mysql" 11 _ "github.com/go-sql-driver/mysql"
12 _ "github.com/mattn/go-sqlite3" 12 _ "github.com/mattn/go-sqlite3"
13 htemplate "html/template" 13 htemplate "html/template"
14 "io/ioutil" 14 "io/ioutil"
15 "log" 15 "log"
16 "math/rand"
16 "net/http" 17 "net/http"
17 "os" 18 "os"
18 "os/exec" 19 "os/exec"
19 "path/filepath" 20 "path/filepath"
20 "regexp" 21 "regexp"
21 "strings" 22 "strings"
22 "text/template" 23 "text/template"
23 "time" 24 "time"
24 ) 25 )
25 26
(...skipping 12 matching lines...) Expand all
38 MAX_TRY_SIZE = 64000 39 MAX_TRY_SIZE = 64000
39 ) 40 )
40 41
41 var ( 42 var (
42 // codeTemplate is the cpp code template the user's code is copied into. 43 // codeTemplate is the cpp code template the user's code is copied into.
43 codeTemplate *template.Template = nil 44 codeTemplate *template.Template = nil
44 45
45 // indexTemplate is the main index.html page we serve. 46 // indexTemplate is the main index.html page we serve.
46 indexTemplate *htemplate.Template = nil 47 indexTemplate *htemplate.Template = nil
47 48
48 » // recentTemplate is a list of recent images. 49 » // recentTemplate is a list of recent images.
49 recentTemplate *htemplate.Template = nil 50 recentTemplate *htemplate.Template = nil
50 51
52 // workspaceTemplate is the page for workspaces, a series of webtrys.
53 workspaceTemplate *htemplate.Template = nil
54
51 // db is the database, nil if we don't have an SQL database to store dat a into. 55 // db is the database, nil if we don't have an SQL database to store dat a into.
52 db *sql.DB = nil 56 db *sql.DB = nil
53 57
54 // directLink is the regex that matches URLs paths that are direct links . 58 // directLink is the regex that matches URLs paths that are direct links .
55 directLink = regexp.MustCompile("^/c/([a-f0-9]+)$") 59 directLink = regexp.MustCompile("^/c/([a-f0-9]+)$")
56 60
57 // imageLink is the regex that matches URLs paths that are direct links to PNGs. 61 // imageLink is the regex that matches URLs paths that are direct links to PNGs.
58 imageLink = regexp.MustCompile("^/i/([a-f0-9]+.png)$") 62 imageLink = regexp.MustCompile("^/i/([a-f0-9]+.png)$")
63
64 // workspaceLink is the regex that matches URLs paths for workspaces.
65 workspaceLink = regexp.MustCompile("^/w/([a-z0-9-]+)$")
66
67 // workspaceNameAdj is a list of adjectives for building workspace names .
68 workspaceNameAdj = []string{
69 "autumn", "hidden", "bitter", "misty", "silent", "empty", "dry", "dark",
70 "summer", "icy", "delicate", "quiet", "white", "cool", "spring", "winter",
71 "patient", "twilight", "dawn", "crimson", "wispy", "weathered", "blue",
72 "billowing", "broken", "cold", "damp", "falling", "frosty", "gre en",
73 "long", "late", "lingering", "bold", "little", "morning", "muddy ", "old",
74 "red", "rough", "still", "small", "sparkling", "throbbing", "shy ",
75 "wandering", "withered", "wild", "black", "young", "holy", "soli tary",
76 "fragrant", "aged", "snowy", "proud", "floral", "restless", "div ine",
77 "polished", "ancient", "purple", "lively", "nameless",
78 }
79
80 // workspaceNameNoun is a list of nouns for building workspace names.
81 workspaceNameNoun = []string{
82 "waterfall", "river", "breeze", "moon", "rain", "wind", "sea", " morning",
83 "snow", "lake", "sunset", "pine", "shadow", "leaf", "dawn", "gli tter",
84 "forest", "hill", "cloud", "meadow", "sun", "glade", "bird", "br ook",
85 "butterfly", "bush", "dew", "dust", "field", "fire", "flower", " firefly",
86 "feather", "grass", "haze", "mountain", "night", "pond", "darkne ss",
87 "snowflake", "silence", "sound", "sky", "shape", "surf", "thunde r",
88 "violet", "water", "wildflower", "wave", "water", "resonance", " sun",
89 "wood", "dream", "cherry", "tree", "fog", "frost", "voice", "pap er",
90 "frog", "smoke", "star",
91 }
59 ) 92 )
60 93
61 // flags 94 // flags
62 var ( 95 var (
63 useChroot = flag.Bool("use_chroot", false, "Run the compiled code in the schroot jail.") 96 useChroot = flag.Bool("use_chroot", false, "Run the compiled code in the schroot jail.")
64 port = flag.String("port", ":8000", "HTTP service address (e.g., ': 8000')") 97 port = flag.String("port", ":8000", "HTTP service address (e.g., ': 8000')")
65 ) 98 )
66 99
67 // lineNumbers adds #line numbering to the user's code. 100 // lineNumbers adds #line numbering to the user's code.
68 func LineNumbers(c string) string { 101 func LineNumbers(c string) string {
(...skipping 14 matching lines...) Expand all
83 if err != nil { 116 if err != nil {
84 log.Fatal(err) 117 log.Fatal(err)
85 } 118 }
86 os.Chdir(cwd) 119 os.Chdir(cwd)
87 120
88 codeTemplate, err = template.ParseFiles(filepath.Join(cwd, "templates/te mplate.cpp")) 121 codeTemplate, err = template.ParseFiles(filepath.Join(cwd, "templates/te mplate.cpp"))
89 if err != nil { 122 if err != nil {
90 panic(err) 123 panic(err)
91 } 124 }
92 // Convert index.html into a template, which is expanded with the code. 125 // Convert index.html into a template, which is expanded with the code.
93 » indexTemplate, err = htemplate.ParseFiles(filepath.Join(cwd, "templates/ index.html")) 126 » indexTemplate, err = htemplate.ParseFiles(
127 » » filepath.Join(cwd, "templates/index.html"),
128 » » filepath.Join(cwd, "templates/titlebar.html"),
129 » )
94 if err != nil { 130 if err != nil {
95 panic(err) 131 panic(err)
96 } 132 }
97 133 » recentTemplate, err = htemplate.ParseFiles(
98 » recentTemplate, err = htemplate.ParseFiles(filepath.Join(cwd, "templates /recent.html")) 134 » » filepath.Join(cwd, "templates/recent.html"),
135 » » filepath.Join(cwd, "templates/titlebar.html"),
136 » )
99 if err != nil { 137 if err != nil {
100 panic(err) 138 panic(err)
101 } 139 }
140 workspaceTemplate, err = htemplate.ParseFiles(
141 filepath.Join(cwd, "templates/workspace.html"),
142 filepath.Join(cwd, "templates/titlebar.html"),
143 )
144 if err != nil {
145 panic(err)
146 }
102 147
103 // Connect to MySQL server. First, get the password from the metadata se rver. 148 // Connect to MySQL server. First, get the password from the metadata se rver.
104 // See https://developers.google.com/compute/docs/metadata#custom. 149 // See https://developers.google.com/compute/docs/metadata#custom.
105 req, err := http.NewRequest("GET", "http://metadata/computeMetadata/v1/i nstance/attributes/password", nil) 150 req, err := http.NewRequest("GET", "http://metadata/computeMetadata/v1/i nstance/attributes/password", nil)
106 if err != nil { 151 if err != nil {
107 panic(err) 152 panic(err)
108 } 153 }
109 client := http.Client{} 154 client := http.Client{}
110 req.Header.Add("X-Google-Metadata-Request", "True") 155 req.Header.Add("X-Google-Metadata-Request", "True")
111 if resp, err := client.Do(req); err == nil { 156 if resp, err := client.Do(req); err == nil {
112 password, err := ioutil.ReadAll(resp.Body) 157 password, err := ioutil.ReadAll(resp.Body)
113 if err != nil { 158 if err != nil {
114 log.Printf("ERROR: Failed to read password from metadata server: %q\n", err) 159 log.Printf("ERROR: Failed to read password from metadata server: %q\n", err)
115 panic(err) 160 panic(err)
116 } 161 }
117 // The IP address of the database is found here: 162 // The IP address of the database is found here:
118 // https://console.developers.google.com/project/31977622648/ sql/instances/webtry/overview 163 // https://console.developers.google.com/project/31977622648/ sql/instances/webtry/overview
119 // And 3306 is the default port for MySQL. 164 // And 3306 is the default port for MySQL.
120 db, err = sql.Open("mysql", fmt.Sprintf("webtry:%s@tcp(173.194.8 3.52:3306)/webtry?parseTime=true", password)) 165 db, err = sql.Open("mysql", fmt.Sprintf("webtry:%s@tcp(173.194.8 3.52:3306)/webtry?parseTime=true", password))
121 if err != nil { 166 if err != nil {
122 log.Printf("ERROR: Failed to open connection to SQL serv er: %q\n", err) 167 log.Printf("ERROR: Failed to open connection to SQL serv er: %q\n", err)
123 panic(err) 168 panic(err)
124 } 169 }
125 } else { 170 } else {
171 log.Printf("INFO: Failed to find metadata, unable to connect to MySQL server (Expected when running locally): %q\n", err)
126 // Fallback to sqlite for local use. 172 // Fallback to sqlite for local use.
127 db, err = sql.Open("sqlite3", "./webtry.db") 173 db, err = sql.Open("sqlite3", "./webtry.db")
128 if err != nil { 174 if err != nil {
129 log.Printf("ERROR: Failed to open: %q\n", err) 175 log.Printf("ERROR: Failed to open: %q\n", err)
130 panic(err) 176 panic(err)
131 } 177 }
132 sql := `CREATE TABLE webtry ( 178 sql := `CREATE TABLE webtry (
133 code TEXT DEFAULT '' NOT NULL, 179 code TEXT DEFAULT '' NOT NULL,
134 create_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, 180 create_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
135 hash CHAR(64) DEFAULT '' NOT NULL, 181 hash CHAR(64) DEFAULT '' NOT NULL,
136 PRIMARY KEY(hash) 182 PRIMARY KEY(hash)
137 )` 183 )`
138 » » db.Exec(sql) 184 » » _, err = db.Exec(sql)
139 » » log.Printf("INFO: Failed to find metadata, unable to connect to MySQL server (Expected when running locally): %q\n", err) 185 » » log.Printf("Info: status creating sqlite table for webtry: %q\n" , err)
186 » » sql = `CREATE TABLE workspace (
187 name TEXT DEFAULT '' NOT NULL,
188 create_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
189 PRIMARY KEY(name)
190 )`
191 » » _, err = db.Exec(sql)
192 » » log.Printf("Info: status creating sqlite table for workspace: %q \n", err)
193 » » sql = `CREATE TABLE workspacetry (
194 name TEXT DEFAULT '' NOT NULL,
195 create_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
196 hash CHAR(64) DEFAULT '' NOT NULL,
197 hidden INTEGER DEFAULT 0 NOT NULL,
198 FOREIGN KEY (name) REFERENCES workspace(name)
199 )`
200 » » _, err = db.Exec(sql)
201 » » log.Printf("Info: status creating sqlite table for workspace try : %q\n", err)
140 } 202 }
141 } 203 }
142 204
143 // userCode is used in template expansion. 205 // userCode is used in template expansion.
144 type userCode struct { 206 type userCode struct {
145 UserCode string 207 UserCode string
146 } 208 }
147 209
148 // expandToFile expands the template and writes the result to the file. 210 // expandToFile expands the template and writes the result to the file.
149 func expandToFile(filename string, code string, t *template.Template) error { 211 func expandToFile(filename string, code string, t *template.Template) error {
(...skipping 74 matching lines...) Expand 10 before | Expand all | Expand 10 after
224 } 286 }
225 log.Printf("Error: %s\n%s", message, err.Error()) 287 log.Printf("Error: %s\n%s", message, err.Error())
226 resp, err := json.Marshal(m) 288 resp, err := json.Marshal(m)
227 if err != nil { 289 if err != nil {
228 http.Error(w, "Failed to serialize a response", 500) 290 http.Error(w, "Failed to serialize a response", 500)
229 return 291 return
230 } 292 }
231 w.Write(resp) 293 w.Write(resp)
232 } 294 }
233 295
234 func writeToDatabase(hash string, code string) { 296 func writeToDatabase(hash string, code string, workspaceName string) {
235 if db == nil { 297 if db == nil {
236 return 298 return
237 } 299 }
238 if _, err := db.Exec("INSERT INTO webtry (code, hash) VALUES(?, ?)", cod e, hash); err != nil { 300 if _, err := db.Exec("INSERT INTO webtry (code, hash) VALUES(?, ?)", cod e, hash); err != nil {
239 log.Printf("ERROR: Failed to insert code into database: %q\n", e rr) 301 log.Printf("ERROR: Failed to insert code into database: %q\n", e rr)
240 } 302 }
303 if workspaceName != "" {
304 if _, err := db.Exec("INSERT INTO workspacetry (name, hash) VALU ES(?, ?)", workspaceName, hash); err != nil {
305 log.Printf("ERROR: Failed to insert into workspacetry ta ble: %q\n", err)
306 }
307 }
241 } 308 }
242 309
243 func cssHandler(w http.ResponseWriter, r *http.Request) { 310 func cssHandler(w http.ResponseWriter, r *http.Request) {
244 http.ServeFile(w, r, "css/webtry.css") 311 http.ServeFile(w, r, "css/webtry.css")
245 } 312 }
246 313
314 func jsHandler(w http.ResponseWriter, r *http.Request) {
315 http.ServeFile(w, r, "js/run.js")
316 }
317
247 // imageHandler serves up the PNG of a specific try. 318 // imageHandler serves up the PNG of a specific try.
248 func imageHandler(w http.ResponseWriter, r *http.Request) { 319 func imageHandler(w http.ResponseWriter, r *http.Request) {
249 log.Printf("Image Handler: %q\n", r.URL.Path) 320 log.Printf("Image Handler: %q\n", r.URL.Path)
250 if r.Method != "GET" { 321 if r.Method != "GET" {
251 http.NotFound(w, r) 322 http.NotFound(w, r)
252 return 323 return
253 } 324 }
254 match := imageLink.FindStringSubmatch(r.URL.Path) 325 match := imageLink.FindStringSubmatch(r.URL.Path)
255 if len(match) != 2 { 326 if len(match) != 2 {
256 http.NotFound(w, r) 327 http.NotFound(w, r)
(...skipping 30 matching lines...) Expand all
287 log.Printf("Error: failed to fetch from database: %q", e rr) 358 log.Printf("Error: failed to fetch from database: %q", e rr)
288 continue 359 continue
289 } 360 }
290 recent = append(recent, Try{Hash: hash, CreateTS: create_ts.Form at("2006-02-01")}) 361 recent = append(recent, Try{Hash: hash, CreateTS: create_ts.Form at("2006-02-01")})
291 } 362 }
292 if err := recentTemplate.Execute(w, Recent{Tries: recent}); err != nil { 363 if err := recentTemplate.Execute(w, Recent{Tries: recent}); err != nil {
293 log.Printf("ERROR: Failed to expand template: %q\n", err) 364 log.Printf("ERROR: Failed to expand template: %q\n", err)
294 } 365 }
295 } 366 }
296 367
368 type Workspace struct {
369 Name string
370 Code string
371 Tries []Try
372 }
373
374 // newWorkspace generates a new random workspace name and stores it in the datab ase.
375 func newWorkspace() (string, error) {
376 for i := 0; i < 10; i++ {
377 adj := workspaceNameAdj[rand.Intn(len(workspaceNameAdj))]
378 noun := workspaceNameNoun[rand.Intn(len(workspaceNameNoun))]
379 suffix := rand.Intn(1000)
380 name := fmt.Sprintf("%s-%s-%d", adj, noun, suffix)
381 if _, err := db.Exec("INSERT INTO workspace (name) VALUES(?)", n ame); err == nil {
382 return name, nil
383 } else {
384 log.Printf("ERROR: Failed to insert workspace into datab ase: %q\n", err)
385 }
386 }
387 return "", fmt.Errorf("Failed to create a new workspace")
388 }
389
390 // getCode returns the code for a given hash, or the empty string if not found.
391 func getCode(hash string) string {
392 code := ""
393 if err := db.QueryRow("SELECT code FROM webtry WHERE hash=?", hash).Scan (&code); err != nil {
394 log.Printf("ERROR: Code for hash is missing: %q\n", err)
395 }
396 return code
397 }
398
399 func workspaceHandler(w http.ResponseWriter, r *http.Request) {
400 log.Printf("Workspace Handler: %q\n", r.URL.Path)
401 if r.Method == "GET" {
402 tries := []Try{}
403 match := workspaceLink.FindStringSubmatch(r.URL.Path)
404 name := ""
405 if len(match) == 2 {
406 name = match[1]
407 rows, err := db.Query("SELECT create_ts, hash FROM works pacetry WHERE name=? ORDER BY create_ts DESC ", name)
408 if err != nil {
409 reportError(w, r, err, "Failed to select.")
410 return
411 }
412 for rows.Next() {
413 var hash string
414 var create_ts time.Time
415 if err := rows.Scan(&create_ts, &hash); err != n il {
416 log.Printf("Error: failed to fetch from database: %q", err)
417 continue
418 }
419 tries = append(tries, Try{Hash: hash, CreateTS: create_ts.Format("2006-02-01")})
420 }
421 }
422 var code string
423 if len(tries) == 0 {
424 code = DEFAULT_SAMPLE
425 } else {
426 code = getCode(tries[len(tries)-1].Hash)
427 }
428 if err := workspaceTemplate.Execute(w, Workspace{Tries: tries, C ode: code, Name: name}); err != nil {
429 log.Printf("ERROR: Failed to expand template: %q\n", err )
430 }
431 } else if r.Method == "POST" {
432 name, err := newWorkspace()
433 if err != nil {
434 http.Error(w, "Failed to create a new workspace.", 500)
435 return
436 }
437 http.Redirect(w, r, "/w/"+name, 302)
438 }
439 }
440
297 // hasPreProcessor returns true if any line in the code begins with a # char. 441 // hasPreProcessor returns true if any line in the code begins with a # char.
298 func hasPreProcessor(code string) bool { 442 func hasPreProcessor(code string) bool {
299 lines := strings.Split(code, "\n") 443 lines := strings.Split(code, "\n")
300 for _, s := range lines { 444 for _, s := range lines {
301 if strings.HasPrefix(strings.TrimSpace(s), "#") { 445 if strings.HasPrefix(strings.TrimSpace(s), "#") {
302 return true 446 return true
303 } 447 }
304 } 448 }
305 return false 449 return false
306 } 450 }
307 451
452 type TryRequest struct {
453 Code string `json:"code"`
454 Name string `json:"name"`
455 }
456
308 // mainHandler handles the GET and POST of the main page. 457 // mainHandler handles the GET and POST of the main page.
309 func mainHandler(w http.ResponseWriter, r *http.Request) { 458 func mainHandler(w http.ResponseWriter, r *http.Request) {
310 log.Printf("Main Handler: %q\n", r.URL.Path) 459 log.Printf("Main Handler: %q\n", r.URL.Path)
311 if r.Method == "GET" { 460 if r.Method == "GET" {
312 code := DEFAULT_SAMPLE 461 code := DEFAULT_SAMPLE
313 match := directLink.FindStringSubmatch(r.URL.Path) 462 match := directLink.FindStringSubmatch(r.URL.Path)
314 if len(match) == 2 && r.URL.Path != "/" { 463 if len(match) == 2 && r.URL.Path != "/" {
315 hash := match[1] 464 hash := match[1]
316 if db == nil { 465 if db == nil {
317 http.NotFound(w, r) 466 http.NotFound(w, r)
(...skipping 15 matching lines...) Expand all
333 n, err := buf.ReadFrom(r.Body) 482 n, err := buf.ReadFrom(r.Body)
334 if err != nil { 483 if err != nil {
335 reportError(w, r, err, "Failed to read a request body.") 484 reportError(w, r, err, "Failed to read a request body.")
336 return 485 return
337 } 486 }
338 if n == MAX_TRY_SIZE { 487 if n == MAX_TRY_SIZE {
339 err := fmt.Errorf("Code length equal to, or exceeded, %d ", MAX_TRY_SIZE) 488 err := fmt.Errorf("Code length equal to, or exceeded, %d ", MAX_TRY_SIZE)
340 reportError(w, r, err, "Code too large.") 489 reportError(w, r, err, "Code too large.")
341 return 490 return
342 } 491 }
343 » » code := string(buf.Bytes()) 492 » » request := TryRequest{}
344 » » if hasPreProcessor(code) { 493 » » if err := json.Unmarshal(buf.Bytes(), &request); err != nil {
494 » » » reportError(w, r, err, "Coulnd't decode JSON.")
495 » » » return
496 » » }
497 » » if hasPreProcessor(request.Code) {
345 err := fmt.Errorf("Found preprocessor macro in code.") 498 err := fmt.Errorf("Found preprocessor macro in code.")
346 reportError(w, r, err, "Preprocessor macros aren't allow ed.") 499 reportError(w, r, err, "Preprocessor macros aren't allow ed.")
347 return 500 return
348 } 501 }
349 » » hash, err := expandCode(LineNumbers(code)) 502 » » hash, err := expandCode(LineNumbers(request.Code))
350 if err != nil { 503 if err != nil {
351 reportError(w, r, err, "Failed to write the code to comp ile.") 504 reportError(w, r, err, "Failed to write the code to comp ile.")
352 return 505 return
353 } 506 }
354 » » writeToDatabase(hash, code) 507 » » writeToDatabase(hash, request.Code, request.Name)
355 message, err := doCmd(fmt.Sprintf(RESULT_COMPILE, hash, hash), t rue) 508 message, err := doCmd(fmt.Sprintf(RESULT_COMPILE, hash, hash), t rue)
356 if err != nil { 509 if err != nil {
357 reportError(w, r, err, "Failed to compile the code:\n"+m essage) 510 reportError(w, r, err, "Failed to compile the code:\n"+m essage)
358 return 511 return
359 } 512 }
360 linkMessage, err := doCmd(fmt.Sprintf(LINK, hash, hash), true) 513 linkMessage, err := doCmd(fmt.Sprintf(LINK, hash, hash), true)
361 if err != nil { 514 if err != nil {
362 reportError(w, r, err, "Failed to link the code:\n"+link Message) 515 reportError(w, r, err, "Failed to link the code:\n"+link Message)
363 return 516 return
364 } 517 }
(...skipping 31 matching lines...) Expand 10 before | Expand all | Expand 10 after
396 reportError(w, r, err, "Failed to serialize a response." ) 549 reportError(w, r, err, "Failed to serialize a response." )
397 return 550 return
398 } 551 }
399 w.Write(resp) 552 w.Write(resp)
400 } 553 }
401 } 554 }
402 555
403 func main() { 556 func main() {
404 flag.Parse() 557 flag.Parse()
405 http.HandleFunc("/i/", imageHandler) 558 http.HandleFunc("/i/", imageHandler)
559 http.HandleFunc("/w/", workspaceHandler)
406 http.HandleFunc("/recent/", recentHandler) 560 http.HandleFunc("/recent/", recentHandler)
407 http.HandleFunc("/css/", cssHandler) 561 http.HandleFunc("/css/", cssHandler)
562 http.HandleFunc("/js/", jsHandler)
408 http.HandleFunc("/", mainHandler) 563 http.HandleFunc("/", mainHandler)
409 log.Fatal(http.ListenAndServe(*port, nil)) 564 log.Fatal(http.ListenAndServe(*port, nil))
410 } 565 }
OLDNEW
« experimental/webtry/DESIGN.md ('K') | « experimental/webtry/templates/workspace.html ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698