Rev 21 | Rev 23 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | Download | RSS feed
#!/usr/bin/env tclsh
# $Id: koakuma.cgi.in 22 2024-10-02 08:00:45Z nishi $
set KOAKUMA_VERSION "1.00"
set components ""
chan configure stdout -buffering none
proc exiting {code} {
exit $code
}
proc loop_components {run} {
global components
foreach {name description version genre} $components {
eval $run
}
}
proc crash {reason} {
global components KOAKUMA_VERSION
puts stderr "----- Start Koakuma Crash dump log -----"
puts stderr "Included components:"
loop_components {
puts stderr " $name: $description, version $version"
}
puts stderr "Reason: $reason"
puts stderr "Code: $::errorCode"
puts stderr "Info: $::errorInfo"
puts stderr "----- End Koakuma Crash dump log -----"
puts "Content-Type: text/html"
puts "Status: 500 Internal Server Error"
puts ""
puts "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 3.2 Final//EN\">"
puts "<html>"
puts " <head>"
puts " <meta http-equiv=\"Content-Type\" content=\"text/html;charset=UTF-8\">"
puts " <title>Oops</title>"
puts " </head>"
puts " <body>"
puts " <h1>Oops</h1>"
puts " <hr>"
puts " Koakuma version $KOAKUMA_VERSION crashed, reason: <code>$reason</code><br>"
puts " See the server error log for details."
puts " </body>"
puts "</html>"
exiting 1
}
if { ![info exists env(PATH_INFO)] } {
puts "Status: 301 Moved Permanently"
puts "Location: $env(SCRIPT_NAME)/"
puts ""
exiting 0
}
if { [file exists "@@PREFIX@@/etc/koakuma/cgi.conf"] } {
if { [catch {
source "@@PREFIX@@/etc/koakuma/cgi.conf"
}] } {
crash "Config failure"
}
}
if { [catch {
set tdom_version "[package require tdom]"
dom createNodeCmd -tagName "rpc" elementNode rootXML
dom createNodeCmd -tagName "project" elementNode keyProject
dom createNodeCmd -tagName "version" -jsonType NONE elementNode keyVersion
dom createNodeCmd -tagName "error" -jsonType NONE elementNode keyError
dom createNodeCmd -tagName "name" -jsonType NONE elementNode keyName
dom createNodeCmd -tagName "description" -jsonType NONE elementNode keyDescription
dom createNodeCmd -tagName "vcs" -jsonType NONE elementNode keyVCS
dom createNodeCmd -tagName "url" -jsonType NONE elementNode keyURL
dom createNodeCmd -jsonType STRING textNode valueString
}] } {
crash "Failed to load tDOM"
}
if { [catch {
set tclx_version "[package require Tclx]"
}] } {
crash "Failed to load TclX"
}
proc Get_KV {lst key} {
foreach {k v} $lst {
if { "$k" == "$key" } {
return "$v"
}
}
return ""
}
proc URL_parse {url} {
if { [regexp {^([^:]+)://(([^:]+:[^@]+|[^:]+:|[^:]+)@)?([^/]+)(.+)?$} "$url" -> scheme userpass_at userpass host path] } {
lappend result "scheme" "$scheme"
lappend result "userpass" "$userpass"
lappend result "host" "$host"
lappend result "path" "$path"
return $result
} elseif { [regexp {^/.+$} "$url" path] } {
lappend result "scheme" "file"
lappend result "userpass" ""
lappend result "host" ""
lappend result "path" "$path"
return $result
}
}
if { [catch {
foreach path [glob @@PREFIX@@/lib/koakuma/component/*.tcl] {
source "$path"
}
}] } {
crash "Could not load components"
}
set data ""
while { [gets stdin line] >= 0 } {
if { "$data" == "" } {
set data "$line"
} else {
set data "$data\n$line"
}
}
chan close stdin
set toc ""
set result ""
set content ""
proc write_db {data} {
set fid [open "@@PREFIX@@/lib/koakuma/db/projects.db" "w"]
puts $fid "$data"
close $fid
}
proc readall_db {} {
set data ""
set fid [open "@@PREFIX@@/lib/koakuma/db/projects.db" "r"]
while { [gets $fid line] >= 0 } {
if { "$data" == "" } {
set data "$line"
} else {
set data "$data\n$line"
}
}
close $fid
return "$data"
}
proc rputs {data} {
global result
if { "$result" == "" } {
set result "$data"
} else {
set result "$result\n$data"
}
}
proc tputs {data} {
global content
if { "$content" == "" } {
set content "$data"
} else {
set content "$content\n$data"
}
}
proc html_escape {data} {
set tmp "[regsub -all {<} "[regsub -all {>} "[string trim "$data"]" {\>}]" {\<}]"
set link "[regsub -all {[^: ]+://[^ \n]+} "$tmp" {<a href="\0">\0</a>}]"
return "[regsub -all {\n} "$link" {<br>}]"
}
proc open_projects {} {
while 1 {
if { ![info exists "@@PREFIX@@/lib/koakuma/db/projects.lock"] } {
break
}
set fid [open "@@PREFIX@@/lib/koakuma/db/projects.lock" "w"]
if { ![info exists "/proc/[gets $fid line]"] } {
close $fid
break
}
after 10
close $fid
}
set fid [open "@@PREFIX@@/lib/koakuma/db/projects.lock" "w"]
puts $fid "[pid]"
close $fid
}
proc scan_projects {run} {
set fid [open "@@PREFIX@@/lib/koakuma/db/projects.db" "r"]
set content ""
while { [gets $fid line] >= 0 } {
if { "$content" == "" } {
set content "$line"
} else {
set content "$content\n$line"
}
}
close $fid
set dom [dom parse "$content"]
set doc [$dom documentElement]
foreach elem [$doc selectNodes "/projects/project"] {
set name "[$elem selectNodes "string(name)"]"
set description "[$elem selectNodes "string(description)"]"
set vcs "[$elem selectNodes "string(vcs)"]"
set vcs_url "[$elem selectNodes "string(url)"]"
eval $run
}
}
proc project_exists {projname} {
set desc ""
scan_projects {
upvar 1 desc desc
upvar 1 projname projname
if { "$name" == "$projname" } {
set desc "$description"
break
}
}
return "$desc"
}
proc close_projects {} {
file delete "@@PREFIX@@/lib/koakuma/db/projects.lock"
}
proc start_html {title has_toc} {
global toc env koakuma_png css
rputs "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\">"
rputs "<html>"
rputs " <head>"
rputs " <meta http-equiv=\"Content-Type\" content=\"text/html;charset=UTF-8\">"
rputs " <title>$title - Koakuma</title>"
rputs " <link rel=\"stylesheet\" href=\"$css\">"
rputs " </head>"
rputs " <body>"
rputs " <a href=\"/koakuma\" id=\"gomain\">"
rputs " <img src=\"$koakuma_png\" alt=\"Koakuma by Kasuya Baian\" height=\"192px\">"
rputs " </a>"
rputs " <div id=\"space\"></div>"
rputs " <div id=\"title\">"
rputs " Koakuma"
rputs " </div>"
rputs " <a href=\"$env(SCRIPT_NAME)\">Root</a>"
rputs " <div id=\"clearfix\"></div>"
if { "$has_toc" == "1" } {
rputs " <div id=\"toc\">"
rputs " <div id=\"toctitle\">TOC</div>"
foreach sect $toc {
if { "[string range "[regsub -all { } "$sect" "-"]" 0 0]" == "-" } {
rputs "<a class=\"shiftlink\" href=\"#TOC-[regsub {^-} "[regsub -all { } "$sect" "-"]" ""]\">[regsub {^-} "$sect" ""]</a><br>"
} else {
rputs "<a href=\"#TOC-[regsub -all { } "$sect" "-"]\">$sect</a><br>"
}
}
rputs " </div>"
}
if { "$has_toc" == "1" } {
rputs " <div id=\"doc\">"
} else {
rputs " <div id=\"doc-notoc\">"
}
rputs " <h1>$title</h1>"
}
proc end_html {has_toc} {
global KOAKUMA_VERSION
rputs " </div>"
rputs " <hr>"
rputs " <i>Powered by <a href=\"http://nishi.boats/koakuma\">Koakuma</a> $KOAKUMA_VERSION</i>"
rputs " </body>"
rputs "</html>"
}
proc add_toc {data} {
global toc
tputs "<h2 id=\"TOC-[regsub -all { } "$data" "-"]\"><a href=\"#TOC-[regsub -all { } "$data" "-"]\">#</a> $data</h2>"
lappend toc "$data"
}
proc add_toc2 {data} {
global toc
tputs "<h3 id=\"TOC-[regsub -all { } "$data" "-"]\"><a href=\"#TOC-[regsub -all { } "$data" "-"]\">#</a> $data</h3>"
lappend toc "-$data"
}
if { [catch {
set path "[regsub -all {/+} "$env(PATH_INFO)" "/"]"
if { [regexp {^/rpc(/.*)?$} "$path"] } {
rputs "Content-Type: application/json"
} else {
if { ![regexp {/$} "$env(PATH_INFO)"] } {
puts "Status: 301 Moved Permanently"
puts "Location: $env(SCRIPT_NAME)$env(PATH_INFO)/"
puts ""
exiting 0
}
rputs "Content-Type: text/html"
}
if { "$path" == "/" } {
add_toc "Tcl Information"
tputs "<table border=\"0\">"
tputs " <tr>"
tputs " <th>"
tputs " Version"
tputs " </th>"
tputs " <td>"
tputs " $tcl_version"
tputs " </td>"
tputs " </tr>"
tputs " <tr>"
tputs " <th>"
tputs " Platform"
tputs " </th>"
tputs " <td>"
tputs " $tcl_platform(os)/$tcl_platform(machine) $tcl_platform(osVersion)"
tputs " </td>"
tputs " </tr>"
tputs " <tr>"
tputs " <th>"
tputs " tDOM version"
tputs " </th>"
tputs " <td>"
tputs " $tdom_version"
tputs " </td>"
tputs " </tr>"
tputs " <tr>"
tputs " <th>"
tputs " TclX version"
tputs " </th>"
tputs " <td>"
tputs " $tclx_version"
tputs " </td>"
tputs " </tr>"
tputs "</table>"
add_toc "Components"
loop_components {
if { [llength [info procs "${name}_info"]] > 0 } {
${name}_info
}
}
set has_projects 0
add_toc "Projects"
open_projects
scan_projects {
upvar 1 has_projects has_projects
if { "$has_projects" == "0" } {
set has_projects 1
tputs "<table border=\"0\">"
}
tputs "<tr>"
tputs " <th><a href=\"/koakuma/project/$name\">$name</a></th>"
tputs " <td>[html_escape "$description"]</td>"
tputs "</tr>"
}
close_projects
if { "$has_projects" == "1" } {
tputs "</table>"
} else {
tputs "No projects have been added, yet."
}
rputs ""
start_html "Main" 1
rputs "$content"
end_html 1
} elseif { [regexp {^/rpc(/.*)?$} "$path"] } {
regexp {^/rpc(/.*)?$} "$path" -> api
set doc [dom createDocumentNode]
$doc appendFromScript {
keyVersion {valueString "$KOAKUMA_VERSION"}
}
if { "$api" == "" || "$api" == "/" } {
rputs ""
rputs "[$doc asJSON]"
} elseif { "$api" == "/launch-job" } {
if { [catch {dom parse -json "$data" clidoc}] } {
rputs "Status: 400 Bad Request"
$doc appendFromScript {
keyError {valueString "Bad JSON"}
}
} else {
set projname "[regsub -all { } "[$clidoc selectNodes "string(/name)"]" "-"]"
set builddesc "[$clidoc selectNodes "string(/description)"]"
if { "$projname" == "" || "$builddesc" == "" } {
rputs "Status: 400 Bad Request"
$doc appendFromScript {
keyError {valueString "Required field missing"}
}
} else {
set has_name 0
set use_vcs ""
set use_vcs_url ""
open_projects
scan_projects {
upvar 1 has_name has_name
upvar 1 projname projname
upvar 1 use_vcs use_vcs
upvar 1 use_vcs_url use_vcs_url
if { "$name" == "$projname" } {
set has_name 1
set use_vcs "$vcs"
set use_vcs_url "$vcs_url"
break
}
}
close_projects
if { $has_name == 0 } {
rputs "Status: 400 Bad Request"
$doc appendFromScript {
keyError {valueString "Project does not exist"}
}
} else {
set cont 1
if { [file exists "@@PREFIX@@/lib/koakuma/db/data/$projname/build.lock"] } {
set fid [open "@@PREFIX@@/lib/koakuma/db/data/$projname/build.lock" "r"]
set readpid "[gets $fid]"
close $fid
if { [file exists "/proc/$readpid"] } {
set cont 0
rputs "Status: 403 Forbidden"
$doc appendFromScript {
keyError {valueString "Other building process has been running"}
}
}
}
if { $cont == 1 } {
set fid [open "@@PREFIX@@/lib/koakuma/db/data/$projname/buildcount" "r"]
set count [expr [gets $fid] + 1]
close $fid
set fid [open "@@PREFIX@@/lib/koakuma/db/data/$projname/buildcount" "w"]
puts $fid "$count"
close $fid
set count "[format %08s "$count"]"
set fid [open "@@PREFIX@@/lib/koakuma/db/data/$projname/lastrun" "w"]
puts $fid "[clock seconds]"
close $fid
file mkdir "@@PREFIX@@/lib/koakuma/db/data/$projname/build-$count"
set pid [fork]
if { $pid } {
set fid [open "@@PREFIX@@/lib/koakuma/db/data/$projname/build.lock" "w"]
puts $fid "$pid"
close $fid
} else {
set fid [open "@@PREFIX@@/lib/koakuma/db/data/$projname/build-$count/log" "w"]
set fail 0
dup $fid stdout
dup $fid stderr
puts "===== Checkout"
puts "Using VCS: $use_vcs"
if { [llength [info procs "${use_vcs}_repository"]] == 0 } {
puts "Component internal failure"
set fail 1
} else {
cd "@@PREFIX@@/lib/koakuma/db/data/$projname"
if { [${use_vcs}_repository "$use_vcs_url" "workspace"] } {
puts "Checkout failure"
set fail 1
}
}
if { $fail == 0 } {
puts "===== Build"
cd "@@PREFIX@@/lib/koakuma/db/data/$projname/workspace"
if { [file exists "@@PREFIX@@/lib/koakuma/db/data/$projname/workspace/Koakumafile"] } {
if { [catch {
namespace eval koakumafile {
source "@@PREFIX@@/lib/koakuma/db/data/$projname/workspace/Koakumafile"
}
koakumafile::run "$projname"
}] } {
puts "Failed to run Koakumafile"
set fail 1
}
} else {
puts "Nothing to do"
}
}
if { $fail == 0 } {
puts "Build successful"
set fidsuc [open "@@PREFIX@@/lib/koakuma/db/data/$projname/lastsuccessfulrun" "w"]
puts $fidsuc "[clock seconds]"
close $fidsuc
set fidsuc [open "@@PREFIX@@/lib/koakuma/db/data/$projname/successbuild" "r"]
set sucbul [gets $fidsuc]
close $fidsuc
set fidsuc [open "@@PREFIX@@/lib/koakuma/db/data/$projname/successbuild" "w"]
puts $fidsuc "[expr $sucbul + 1]"
close $fidsuc
}
close $fid
file delete "@@PREFIX@@/lib/koakuma/db/data/$projname/build.lock"
exit 0
}
}
}
}
}
rputs ""
rputs "[$doc asJSON]"
} elseif { "$api" == "/create-project" } {
if { [catch {dom parse -json "$data" clidoc}] } {
rputs "Status: 400 Bad Request"
$doc appendFromScript {
keyError {valueString "Bad JSON"}
}
} else {
set projname "[regsub -all { } "[$clidoc selectNodes "string(/name)"]" "-"]"
set projdescription "[$clidoc selectNodes "string(/description)"]"
set projvcs "[$clidoc selectNodes "string(/vcs)"]"
set url "[$clidoc selectNodes "string(/url)"]"
if { "$projname" == "" || "$projdescription" == "" || "$projvcs" == "" || "$url" == "" } {
rputs "Status: 400 Bad Request"
$doc appendFromScript {
keyError {valueString "Required field missing"}
}
} else {
set has_vcs 0
set has_name 0
loop_components {
upvar 1 has_vcs has_vcs
upvar 1 projvcs projvcs
if { "$name" == "$projvcs" && "$genre" == "VCS" } {
set has_vcs 1
break
}
}
open_projects
scan_projects {
upvar 1 has_name has_name
upvar 1 projname projname
if { "$name" == "$projname" } {
set has_name 1
break
}
}
close_projects
if { $has_vcs == 0 } {
rputs "Status: 400 Bad Request"
$doc appendFromScript {
keyError {valueString "Not a valid VCS"}
}
} elseif { $has_name == 1 } {
rputs "Status: 400 Bad Request"
$doc appendFromScript {
keyError {valueString "Project already exists"}
}
} else {
open_projects
set xml "[readall_db]"
set xmldoc [dom parse "$xml"]
set root [$xmldoc documentElement]
$root appendFromScript {
keyProject {
keyName {valueString "$projname"}
keyDescription {valueString "$projdescription"}
keyVCS {valueString "$projvcs"}
keyURL {valueString "$url"}
}
}
write_db "[$xmldoc asXML]"
file mkdir "@@PREFIX@@/lib/koakuma/db/data/$projname"
set fid [open "@@PREFIX@@/lib/koakuma/db/data/$projname/buildcount" "w"]
puts $fid "0"
close $fid
set fid [open "@@PREFIX@@/lib/koakuma/db/data/$projname/successbuild" "w"]
puts $fid "0"
close $fid
close_projects
}
}
}
rputs ""
rputs "[$doc asJSON]"
} else {
$root appendFromScript {
keyError {valueString "No such endpoint"}
}
rputs "Status: 404 Not Found"
rputs ""
rputs "[$doc asJSON]"
}
} elseif { [regexp {^/project/[^/]+.*$} "$path"] } {
regexp {^/project/([^/]+)(.*)$} "$path" -> projname projpath
open_projects
set has_project [project_exists "$projname"]
close_projects
if { "$has_project" != "" } {
if { "$projpath" == "" || "$projpath" == "/" } {
add_toc "Description"
tputs "[html_escape "$has_project"]"
add_toc "Details"
tputs "<table border=\"0\">"
tputs " <tr>"
tputs " <th>"
tputs " Status"
tputs " </th>"
tputs " <td>"
if { [file exists "@@PREFIX@@/lib/koakuma/db/data/$projname/build.lock"] } {
set fid [open "@@PREFIX@@/lib/koakuma/db/data/$projname/build.lock" "r"]
if { [file exists "/proc/[gets $fid]"] } {
tputs "Running"
} else {
tputs "Idle"
}
close $fid
} else {
tputs "Idle"
}
tputs " "
tputs " </td>"
tputs " </tr>"
tputs " <tr>"
tputs " <th>"
tputs " Last run"
tputs " </th>"
tputs " <td>"
if { [file exists "@@PREFIX@@/lib/koakuma/db/data/$projname/lastrun"] } {
set fid [open "@@PREFIX@@/lib/koakuma/db/data/$projname/lastrun" "r"]
set date "[clock format "[gets $fid]" -format "%a %b %d %H:%M:%S %Z %Y"]"
close $fid
tputs "$date"
} else {
tputs "No builds yet"
}
tputs " "
tputs " </td>"
tputs " </tr>"
tputs " <tr>"
tputs " <th>"
tputs " Last successful run"
tputs " </th>"
tputs " <td>"
if { [file exists "@@PREFIX@@/lib/koakuma/db/data/$projname/lastsuccessfulrun"] } {
set fid [open "@@PREFIX@@/lib/koakuma/db/data/$projname/lastsuccessfulrun" "r"]
set date "[clock format "[gets $fid]" -format "%a %b %d %H:%M:%S %Z %Y"]"
close $fid
tputs "$date"
} else {
tputs "No successful builds yet"
}
tputs " "
tputs " </td>"
tputs " </tr>"
set builds [lsort -ascii [glob -nocomplain "@@PREFIX@@/lib/koakuma/db/data/$projname/build-*"]]
if { [llength $builds] > 0 } {
tputs " <tr>"
tputs " <th>"
tputs " Successful builds"
tputs " </th>"
tputs " <td>"
if { [file exists "@@PREFIX@@/lib/koakuma/db/data/$projname/successbuild"] } {
set fid [open "@@PREFIX@@/lib/koakuma/db/data/$projname/successbuild" "r"]
tputs "[format %.2f [expr [gets $fid] / [llength $builds] * 100]]%"
close $fid
}
tputs " "
tputs " </td>"
tputs " </tr>"
}
tputs "</table>"
set builds [lsort -ascii [glob -nocomplain "@@PREFIX@@/lib/koakuma/db/data/$projname/build-*"]]
if { [llength $builds] > 0 } {
add_toc "Last build log"
set lastbuild "[lindex $builds [expr [llength $builds] - 1]]"
set fid [open "$lastbuild/log" "r"]
tputs "<pre>"
while { [gets $fid line] >= 0 } {
tputs "[html_escape "$line"]"
}
tputs "</pre>"
close $fid
}
rputs ""
start_html "Project: $projname" 1
rputs "$content"
end_html 1
} else {
tputs "I could not find the endpoint you were finding."
rputs "Status: 404 Not Found"
rputs ""
start_html "Project: $projname" 1
rputs "$content"
end_html 1
}
} else {
tputs "I could not find the project you were finding."
rputs "Status: 404 Not Found"
rputs ""
start_html "Not Found" 0
rputs "$content"
end_html 0
}
} else {
tputs "I could not find the content you were finding."
rputs "Status: 404 Not Found"
rputs ""
start_html "Not Found" 0
rputs "$content"
end_html 0
}
}] } {
crash "Could not render the HTML"
} else {
puts "$result"
}
exiting 0