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

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: rebase 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
« no previous file with comments | « experimental/webtry/templates/workspace.html ('k') | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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 CHAR(64) 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 CHAR(64) 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
199 FOREIGN KEY (name) REFERENCES workspace(name)
200 )`
201 » » _, err = db.Exec(sql)
202 » » log.Printf("Info: status creating sqlite table for workspace try : %q\n", err)
140 } 203 }
141 } 204 }
142 205
143 // userCode is used in template expansion. 206 // userCode is used in template expansion.
144 type userCode struct { 207 type userCode struct {
145 UserCode string 208 UserCode string
146 } 209 }
147 210
148 // expandToFile expands the template and writes the result to the file. 211 // expandToFile expands the template and writes the result to the file.
149 func expandToFile(filename string, code string, t *template.Template) error { 212 func expandToFile(filename string, code string, t *template.Template) error {
(...skipping 74 matching lines...) Expand 10 before | Expand all | Expand 10 after
224 } 287 }
225 log.Printf("Error: %s\n%s", message, err.Error()) 288 log.Printf("Error: %s\n%s", message, err.Error())
226 resp, err := json.Marshal(m) 289 resp, err := json.Marshal(m)
227 if err != nil { 290 if err != nil {
228 http.Error(w, "Failed to serialize a response", 500) 291 http.Error(w, "Failed to serialize a response", 500)
229 return 292 return
230 } 293 }
231 w.Write(resp) 294 w.Write(resp)
232 } 295 }
233 296
234 func writeToDatabase(hash string, code string) { 297 func writeToDatabase(hash string, code string, workspaceName string) {
235 if db == nil { 298 if db == nil {
236 return 299 return
237 } 300 }
238 if _, err := db.Exec("INSERT INTO webtry (code, hash) VALUES(?, ?)", cod e, hash); err != nil { 301 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) 302 log.Printf("ERROR: Failed to insert code into database: %q\n", e rr)
240 } 303 }
304 if workspaceName != "" {
305 if _, err := db.Exec("INSERT INTO workspacetry (name, hash) VALU ES(?, ?)", workspaceName, hash); err != nil {
306 log.Printf("ERROR: Failed to insert into workspacetry ta ble: %q\n", err)
307 }
308 }
241 } 309 }
242 310
243 func cssHandler(w http.ResponseWriter, r *http.Request) { 311 func cssHandler(w http.ResponseWriter, r *http.Request) {
244 http.ServeFile(w, r, "css/webtry.css") 312 http.ServeFile(w, r, "css/webtry.css")
245 } 313 }
246 314
315 func jsHandler(w http.ResponseWriter, r *http.Request) {
316 http.ServeFile(w, r, "js/run.js")
317 }
318
247 // imageHandler serves up the PNG of a specific try. 319 // imageHandler serves up the PNG of a specific try.
248 func imageHandler(w http.ResponseWriter, r *http.Request) { 320 func imageHandler(w http.ResponseWriter, r *http.Request) {
249 log.Printf("Image Handler: %q\n", r.URL.Path) 321 log.Printf("Image Handler: %q\n", r.URL.Path)
250 if r.Method != "GET" { 322 if r.Method != "GET" {
251 http.NotFound(w, r) 323 http.NotFound(w, r)
252 return 324 return
253 } 325 }
254 match := imageLink.FindStringSubmatch(r.URL.Path) 326 match := imageLink.FindStringSubmatch(r.URL.Path)
255 if len(match) != 2 { 327 if len(match) != 2 {
256 http.NotFound(w, r) 328 http.NotFound(w, r)
(...skipping 30 matching lines...) Expand all
287 log.Printf("Error: failed to fetch from database: %q", e rr) 359 log.Printf("Error: failed to fetch from database: %q", e rr)
288 continue 360 continue
289 } 361 }
290 recent = append(recent, Try{Hash: hash, CreateTS: create_ts.Form at("2006-02-01")}) 362 recent = append(recent, Try{Hash: hash, CreateTS: create_ts.Form at("2006-02-01")})
291 } 363 }
292 if err := recentTemplate.Execute(w, Recent{Tries: recent}); err != nil { 364 if err := recentTemplate.Execute(w, Recent{Tries: recent}); err != nil {
293 log.Printf("ERROR: Failed to expand template: %q\n", err) 365 log.Printf("ERROR: Failed to expand template: %q\n", err)
294 } 366 }
295 } 367 }
296 368
369 type Workspace struct {
370 Name string
371 Code string
372 Tries []Try
373 }
374
375 // newWorkspace generates a new random workspace name and stores it in the datab ase.
376 func newWorkspace() (string, error) {
377 for i := 0; i < 10; i++ {
378 adj := workspaceNameAdj[rand.Intn(len(workspaceNameAdj))]
379 noun := workspaceNameNoun[rand.Intn(len(workspaceNameNoun))]
380 suffix := rand.Intn(1000)
381 name := fmt.Sprintf("%s-%s-%d", adj, noun, suffix)
382 if _, err := db.Exec("INSERT INTO workspace (name) VALUES(?)", n ame); err == nil {
383 return name, nil
384 } else {
385 log.Printf("ERROR: Failed to insert workspace into datab ase: %q\n", err)
386 }
387 }
388 return "", fmt.Errorf("Failed to create a new workspace")
389 }
390
391 // getCode returns the code for a given hash, or the empty string if not found.
392 func getCode(hash string) string {
393 code := ""
394 if err := db.QueryRow("SELECT code FROM webtry WHERE hash=?", hash).Scan (&code); err != nil {
395 log.Printf("ERROR: Code for hash is missing: %q\n", err)
396 }
397 return code
398 }
399
400 func workspaceHandler(w http.ResponseWriter, r *http.Request) {
401 log.Printf("Workspace Handler: %q\n", r.URL.Path)
402 if r.Method == "GET" {
403 tries := []Try{}
404 match := workspaceLink.FindStringSubmatch(r.URL.Path)
405 name := ""
406 if len(match) == 2 {
407 name = match[1]
408 rows, err := db.Query("SELECT create_ts, hash FROM works pacetry WHERE name=? ORDER BY create_ts DESC ", name)
409 if err != nil {
410 reportError(w, r, err, "Failed to select.")
411 return
412 }
413 for rows.Next() {
414 var hash string
415 var create_ts time.Time
416 if err := rows.Scan(&create_ts, &hash); err != n il {
417 log.Printf("Error: failed to fetch from database: %q", err)
418 continue
419 }
420 tries = append(tries, Try{Hash: hash, CreateTS: create_ts.Format("2006-02-01")})
421 }
422 }
423 var code string
424 if len(tries) == 0 {
425 code = DEFAULT_SAMPLE
426 } else {
427 code = getCode(tries[len(tries)-1].Hash)
428 }
429 if err := workspaceTemplate.Execute(w, Workspace{Tries: tries, C ode: code, Name: name}); err != nil {
430 log.Printf("ERROR: Failed to expand template: %q\n", err )
431 }
432 } else if r.Method == "POST" {
433 name, err := newWorkspace()
434 if err != nil {
435 http.Error(w, "Failed to create a new workspace.", 500)
436 return
437 }
438 http.Redirect(w, r, "/w/"+name, 302)
439 }
440 }
441
297 // hasPreProcessor returns true if any line in the code begins with a # char. 442 // hasPreProcessor returns true if any line in the code begins with a # char.
298 func hasPreProcessor(code string) bool { 443 func hasPreProcessor(code string) bool {
299 lines := strings.Split(code, "\n") 444 lines := strings.Split(code, "\n")
300 for _, s := range lines { 445 for _, s := range lines {
301 if strings.HasPrefix(strings.TrimSpace(s), "#") { 446 if strings.HasPrefix(strings.TrimSpace(s), "#") {
302 return true 447 return true
303 } 448 }
304 } 449 }
305 return false 450 return false
306 } 451 }
307 452
453 type TryRequest struct {
454 Code string `json:"code"`
455 Name string `json:"name"`
456 }
457
308 // mainHandler handles the GET and POST of the main page. 458 // mainHandler handles the GET and POST of the main page.
309 func mainHandler(w http.ResponseWriter, r *http.Request) { 459 func mainHandler(w http.ResponseWriter, r *http.Request) {
310 log.Printf("Main Handler: %q\n", r.URL.Path) 460 log.Printf("Main Handler: %q\n", r.URL.Path)
311 if r.Method == "GET" { 461 if r.Method == "GET" {
312 code := DEFAULT_SAMPLE 462 code := DEFAULT_SAMPLE
313 match := directLink.FindStringSubmatch(r.URL.Path) 463 match := directLink.FindStringSubmatch(r.URL.Path)
314 if len(match) == 2 && r.URL.Path != "/" { 464 if len(match) == 2 && r.URL.Path != "/" {
315 hash := match[1] 465 hash := match[1]
316 if db == nil { 466 if db == nil {
317 http.NotFound(w, r) 467 http.NotFound(w, r)
(...skipping 15 matching lines...) Expand all
333 n, err := buf.ReadFrom(r.Body) 483 n, err := buf.ReadFrom(r.Body)
334 if err != nil { 484 if err != nil {
335 reportError(w, r, err, "Failed to read a request body.") 485 reportError(w, r, err, "Failed to read a request body.")
336 return 486 return
337 } 487 }
338 if n == MAX_TRY_SIZE { 488 if n == MAX_TRY_SIZE {
339 err := fmt.Errorf("Code length equal to, or exceeded, %d ", MAX_TRY_SIZE) 489 err := fmt.Errorf("Code length equal to, or exceeded, %d ", MAX_TRY_SIZE)
340 reportError(w, r, err, "Code too large.") 490 reportError(w, r, err, "Code too large.")
341 return 491 return
342 } 492 }
343 » » code := string(buf.Bytes()) 493 » » request := TryRequest{}
344 » » if hasPreProcessor(code) { 494 » » if err := json.Unmarshal(buf.Bytes(), &request); err != nil {
495 » » » reportError(w, r, err, "Coulnd't decode JSON.")
496 » » » return
497 » » }
498 » » if hasPreProcessor(request.Code) {
345 err := fmt.Errorf("Found preprocessor macro in code.") 499 err := fmt.Errorf("Found preprocessor macro in code.")
346 reportError(w, r, err, "Preprocessor macros aren't allow ed.") 500 reportError(w, r, err, "Preprocessor macros aren't allow ed.")
347 return 501 return
348 } 502 }
349 » » hash, err := expandCode(LineNumbers(code)) 503 » » hash, err := expandCode(LineNumbers(request.Code))
350 if err != nil { 504 if err != nil {
351 reportError(w, r, err, "Failed to write the code to comp ile.") 505 reportError(w, r, err, "Failed to write the code to comp ile.")
352 return 506 return
353 } 507 }
354 » » writeToDatabase(hash, code) 508 » » writeToDatabase(hash, request.Code, request.Name)
355 message, err := doCmd(fmt.Sprintf(RESULT_COMPILE, hash, hash), t rue) 509 message, err := doCmd(fmt.Sprintf(RESULT_COMPILE, hash, hash), t rue)
356 if err != nil { 510 if err != nil {
357 reportError(w, r, err, "Failed to compile the code:\n"+m essage) 511 reportError(w, r, err, "Failed to compile the code:\n"+m essage)
358 return 512 return
359 } 513 }
360 linkMessage, err := doCmd(fmt.Sprintf(LINK, hash, hash), true) 514 linkMessage, err := doCmd(fmt.Sprintf(LINK, hash, hash), true)
361 if err != nil { 515 if err != nil {
362 reportError(w, r, err, "Failed to link the code:\n"+link Message) 516 reportError(w, r, err, "Failed to link the code:\n"+link Message)
363 return 517 return
364 } 518 }
(...skipping 31 matching lines...) Expand 10 before | Expand all | Expand 10 after
396 reportError(w, r, err, "Failed to serialize a response." ) 550 reportError(w, r, err, "Failed to serialize a response." )
397 return 551 return
398 } 552 }
399 w.Write(resp) 553 w.Write(resp)
400 } 554 }
401 } 555 }
402 556
403 func main() { 557 func main() {
404 flag.Parse() 558 flag.Parse()
405 http.HandleFunc("/i/", imageHandler) 559 http.HandleFunc("/i/", imageHandler)
560 http.HandleFunc("/w/", workspaceHandler)
406 http.HandleFunc("/recent/", recentHandler) 561 http.HandleFunc("/recent/", recentHandler)
407 http.HandleFunc("/css/", cssHandler) 562 http.HandleFunc("/css/", cssHandler)
563 http.HandleFunc("/js/", jsHandler)
408 http.HandleFunc("/", mainHandler) 564 http.HandleFunc("/", mainHandler)
409 log.Fatal(http.ListenAndServe(*port, nil)) 565 log.Fatal(http.ListenAndServe(*port, nil))
410 } 566 }
OLDNEW
« no previous file with comments | « experimental/webtry/templates/workspace.html ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698