├── public ├── css │ ├── boilerplate.css │ ├── log.css │ └── style.css └── images │ ├── download.png │ ├── error.png │ ├── icons.png │ ├── progress.gif │ └── tick.png ├── readme.markdown ├── src ├── builder.nim ├── builder.nim.cfg ├── db.nim ├── github.nim ├── htmlhelp.nim ├── index.html ├── ircbot.nim ├── irclog.nim ├── irclogrender.nim ├── types.nim ├── website.nim └── website.nim.cfg ├── structure.markdown ├── tests └── dummyhub.nim └── todo.markdown /public/css/boilerplate.css: -------------------------------------------------------------------------------- 1 | 2 | /* ==== Scroll down to find where to put your styles :) ==== */ 3 | 4 | /* HTML5 ✰ Boilerplate */ 5 | 6 | html, body, div, span, object, iframe, 7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 8 | abbr, address, cite, code, del, dfn, em, img, ins, kbd, q, samp, 9 | small, strong, sub, sup, var, b, i, dl, dt, dd, ol, ul, li, 10 | fieldset, form, label, legend, 11 | table, caption, tbody, tfoot, thead, tr, th, td, 12 | article, aside, canvas, details, figcaption, figure, 13 | footer, header, hgroup, menu, nav, section, summary, 14 | time, mark, audio, video { 15 | margin: 0; 16 | padding: 0; 17 | border: 0; 18 | font-size: 100%; 19 | font: inherit; 20 | vertical-align: baseline; 21 | } 22 | 23 | article, aside, details, figcaption, figure, 24 | footer, header, hgroup, menu, nav, section { 25 | display: block; 26 | } 27 | 28 | blockquote, q { quotes: none; } 29 | blockquote:before, blockquote:after, 30 | q:before, q:after { content: ''; content: none; } 31 | ins { background-color: #ff9; color: #000; text-decoration: none; } 32 | mark { background-color: #ff9; color: #000; font-style: italic; font-weight: bold; } 33 | del { text-decoration: line-through; } 34 | abbr[title], dfn[title] { border-bottom: 1px dotted; cursor: help; } 35 | table { border-collapse: collapse; border-spacing: 0; } 36 | hr { display: block; height: 1px; border: 0; border-top: 1px solid #ccc; margin: 1em 0; padding: 0; } 37 | input, select { vertical-align: middle; } 38 | 39 | body { font:13px/1.231 sans-serif; *font-size:small; } 40 | select, input, textarea, button { font:99% sans-serif; } 41 | pre, code, kbd, samp { font-family: monospace, sans-serif; } 42 | 43 | html { overflow-y: scroll; } 44 | a:hover, a:active { outline: none; } 45 | ul, ol { margin-left: 2em; } 46 | ol { list-style-type: decimal; } 47 | nav ul, nav li { margin: 0; list-style:none; list-style-image: none; } 48 | small { font-size: 85%; } 49 | strong, th { font-weight: bold; } 50 | td { vertical-align: top; } 51 | 52 | sub, sup { font-size: 75%; line-height: 0; position: relative; } 53 | sup { top: -0.5em; } 54 | sub { bottom: -0.25em; } 55 | 56 | pre { white-space: pre; white-space: pre-wrap; word-wrap: break-word; padding: 15px; } 57 | textarea { overflow: auto; } 58 | .ie6 legend, .ie7 legend { margin-left: -7px; } 59 | input[type="radio"] { vertical-align: text-bottom; } 60 | input[type="checkbox"] { vertical-align: bottom; } 61 | .ie7 input[type="checkbox"] { vertical-align: baseline; } 62 | .ie6 input { vertical-align: text-bottom; } 63 | label, input[type="button"], input[type="submit"], input[type="image"], button { cursor: pointer; } 64 | button, input, select, textarea { margin: 0; } 65 | input:valid, textarea:valid { } 66 | input:invalid, textarea:invalid { border-radius: 1px; -moz-box-shadow: 0px 0px 5px red; -webkit-box-shadow: 0px 0px 5px red; box-shadow: 0px 0px 5px red; } 67 | .no-boxshadow input:invalid, .no-boxshadow textarea:invalid { background-color: #f0dddd; } 68 | 69 | a:link { -webkit-tap-highlight-color: #FF5E99; } 70 | 71 | button { width: auto; overflow: visible; } 72 | .ie7 img { -ms-interpolation-mode: bicubic; } 73 | 74 | body, select, input, textarea { color: #444; } 75 | h1, h2, h3, h4, h5, h6 { font-weight: bold; } 76 | a, a:active, a:visited { color: #607890; } 77 | a:hover { color: #036; } 78 | 79 | /* 80 | // ========================================== \\ 81 | || || 82 | || Your styles ! || 83 | || || 84 | \\ ========================================== // 85 | */ 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | .ir { display: block; text-indent: -999em; overflow: hidden; background-repeat: no-repeat; text-align: left; direction: ltr; } 102 | .hidden { display: none; visibility: hidden; } 103 | .visuallyhidden { border: 0; clip: rect(0 0 0 0); height: 1px; margin: -1px; overflow: hidden; padding: 0; position: absolute; width: 1px; } 104 | .visuallyhidden.focusable:active, 105 | .visuallyhidden.focusable:focus { clip: auto; height: auto; margin: 0; overflow: visible; position: static; width: auto; } 106 | .invisible { visibility: hidden; } 107 | .clearfix:before, .clearfix:after { content: "\0020"; display: block; height: 0; overflow: hidden; } 108 | .clearfix:after { clear: both; } 109 | .clearfix { zoom: 1; } 110 | 111 | 112 | @media all and (orientation:portrait) { 113 | 114 | } 115 | 116 | @media all and (orientation:landscape) { 117 | 118 | } 119 | 120 | @media screen and (max-device-width: 480px) { 121 | 122 | /* html { -webkit-text-size-adjust:none; -ms-text-size-adjust:none; } */ 123 | } 124 | 125 | 126 | @media print { 127 | * { background: transparent !important; color: black !important; text-shadow: none !important; filter:none !important; 128 | -ms-filter: none !important; } 129 | a, a:visited { color: #444 !important; text-decoration: underline; } 130 | a[href]:after { content: " (" attr(href) ")"; } 131 | abbr[title]:after { content: " (" attr(title) ")"; } 132 | .ir a:after, a[href^="javascript:"]:after, a[href^="#"]:after { content: ""; } 133 | pre, blockquote { border: 1px solid #999; page-break-inside: avoid; } 134 | thead { display: table-header-group; } 135 | tr, img { page-break-inside: avoid; } 136 | @page { margin: 0.5cm; } 137 | p, h2, h3 { orphans: 3; widows: 3; } 138 | h2, h3{ page-break-after: avoid; } 139 | } 140 | -------------------------------------------------------------------------------- /public/css/log.css: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | background-color: #2d2d2d; 4 | color: #ffffff; 5 | font-size: 12pt; 6 | } 7 | 8 | table { 9 | margin-top: 15pt; 10 | 11 | } 12 | 13 | table td, table th { 14 | padding: 0.3em 0.4em; 15 | } 16 | 17 | td.nick { 18 | border-left: 1px solid #CC880A; 19 | border-right: 1px solid #CC880A; 20 | text-align: right; 21 | } 22 | 23 | tr.join td.msg, tr.join td.nick { 24 | color: #33DB09; 25 | } 26 | 27 | tr.quit td.msg, tr.quit td.nick { 28 | color: #DB2109; 29 | } 30 | 31 | tr.nick td.msg, tr.nick td.nick { 32 | color: #098BDB; 33 | } 34 | 35 | tr.part td.msg, tr.part td.nick { 36 | color: #DB9909; 37 | } 38 | 39 | tr.action td.msg, tr.action td.nick { 40 | color: #C609DB; 41 | } 42 | 43 | a:link { 44 | color: #F2A20C; 45 | 46 | } 47 | 48 | a:hover { 49 | color: #CC880A; 50 | } 51 | 52 | #controls { 53 | font-size: 16pt; 54 | text-align: center; 55 | } 56 | 57 | hr { 58 | background-color: #CC880A; 59 | border:0; 60 | height: 2px; 61 | } -------------------------------------------------------------------------------- /public/css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-size: medium; 3 | } 4 | 5 | body,html { 6 | height: 100%; 7 | } 8 | 9 | a { 10 | /* text-decoration: none; */ 11 | /* That looks interesting */ 12 | } 13 | 14 | #wrapper { 15 | height: auto !important; 16 | margin: 0 auto -21pt; /* For footer */ 17 | min-height: 100%; 18 | } 19 | 20 | div#header { 21 | font-size: 2em; 22 | background-color: #3d3d3d; 23 | border-bottom: solid 2px #000000; 24 | padding: 0.25em; 25 | color: #ffffff; 26 | } 27 | 28 | div#content { 29 | margin: 0.5em; 30 | } 31 | 32 | table { 33 | text-align: left; 34 | margin-top: 1em; 35 | margin-bottom: 0.5em; 36 | font-size: 9pt; 37 | border-collapse: separate; /* Fighting with boilerplate.css here. */ 38 | } 39 | 40 | table td, table th { 41 | padding: 0.45em 0.4em; 42 | } 43 | 44 | td { 45 | border-right: 1px solid #E0E0E0; 46 | } 47 | 48 | th { 49 | background-color: #5D5D5D; 50 | background: -moz-linear-gradient(top, #5D5D5D, #4D4D4D); 51 | background: -webkit-linear-gradient(top, #5D5D5D, #4D4D4D); 52 | background: -o-linear-gradient(top, #5D5D5D, #4D4D4D); 53 | color: #FFFFFF; 54 | text-align: center; 55 | 56 | border-right: 1px solid #3d3d3d; 57 | border-bottom: 1px solid #3d3d3d; 58 | } 59 | 60 | tr:nth-child(even) { 61 | background-color: #eee; 62 | } 63 | 64 | table#downloads td.green { 65 | background: -moz-linear-gradient(top, #00B40C, #03A90E); 66 | background: -webkit-linear-gradient(top, #00B40C, #03A90E); 67 | background: -o-linear-gradient(top, #00B40C, #03A90E); 68 | 69 | border-right: 1px solid #148420; 70 | border-bottom: 1px solid #148420; 71 | 72 | color: #ffffff; 73 | } 74 | 75 | table#downloads td.orange { 76 | background: -moz-linear-gradient(top, #DE9116, #CC8512); 77 | background: -webkit-linear-gradient(top, #DE9116, #CC8512); 78 | background: -o-linear-gradient(top, #DE9116, #CC8512); 79 | 80 | border-right: 1px solid #A86E0F; 81 | border-bottom: 1px solid #A86E0F; 82 | 83 | color: #ffffff; 84 | } 85 | 86 | table#downloads td:hover { 87 | background: -moz-linear-gradient(top, #0099c7, #0294C1); 88 | background: -webkit-linear-gradient(top, #0099c7, #0294C1); 89 | background: -o-linear-gradient(top, #0099c7, #0294C1); 90 | 91 | border-right: solid 1px #077A9C; 92 | border-bottom: solid 1px #077A9C; 93 | 94 | cursor: pointer; 95 | } 96 | 97 | table#downloads td a { 98 | color: #ffffff; 99 | text-decoration: none; 100 | } 101 | 102 | 103 | /* Awesome buttons :P */ 104 | 105 | a.button { 106 | border-radius: 2px 2px 2px 2px; 107 | background: -moz-linear-gradient(top, #f7f7f7, #ebebeb); 108 | background: -webkit-linear-gradient(top, #f7f7f7, #ebebeb); 109 | background: -o-linear-gradient(top, #f7f7f7, #ebebeb); 110 | text-decoration: none; 111 | color: #3d3d3d; 112 | padding: 5px; 113 | border: solid 1px #9d9d9d; 114 | display: inline-block; 115 | position: relative; 116 | text-align: center; 117 | font-size: small; 118 | } 119 | 120 | a.button.active { 121 | background: -moz-linear-gradient(top, #00B40C, #03A90E); 122 | background: -webkit-linear-gradient(top, #00B40C, #03A90E); 123 | background: -o-linear-gradient(top, #00B40C, #03A90E); 124 | border: solid 1px #148420; 125 | color: #ffffff; 126 | } 127 | 128 | a.button.warning { 129 | background: -moz-linear-gradient(top, #DE9116, #CC8512); 130 | background: -webkit-linear-gradient(top, #DE9116, #CC8512); 131 | background: -o-linear-gradient(top, #DE9116, #CC8512); 132 | border: solid 1px #A86E0F; 133 | color: #ffffff; 134 | } 135 | 136 | a.button.left { 137 | border-top-right-radius: 0; 138 | border-bottom-right-radius: 0; 139 | } 140 | 141 | a.button.middle { 142 | border-radius: 0; 143 | border-left: 0; 144 | } 145 | 146 | a.button.right { 147 | border-top-left-radius: 0; 148 | border-bottom-left-radius: 0; 149 | border-left: 0; 150 | } 151 | 152 | a.button:hover { 153 | background: -moz-linear-gradient(top, #0099c7, #0294C1); 154 | background: -webkit-linear-gradient(top, #0099c7, #0294C1); 155 | background: -o-linear-gradient(top, #0099c7, #0294C1); 156 | border: solid 1px #077A9C; 157 | color: #ffffff; 158 | } 159 | 160 | a.button.middle:hover, a.button.right:hover { 161 | border-left: 0; 162 | } 163 | 164 | a.button span.download { 165 | background-image: url("../images/icons.png"); 166 | background-repeat: no-repeat; 167 | display: inline-block; 168 | margin: auto 3px auto auto; 169 | height: 15px; 170 | width: 14px; 171 | position: relative; 172 | background-position: 0 -30px; 173 | top: 3px; 174 | } 175 | 176 | a.button span.book { 177 | background-image: url("../images/icons.png"); 178 | background-repeat: no-repeat; 179 | display: inline-block; 180 | margin: auto 3px auto auto; 181 | height: 15px; 182 | width: 14px; 183 | position: relative; 184 | background-position: 0 0; 185 | top: 3px; 186 | } 187 | 188 | a.button.active span.download, a.button.warning span.download, a.button:hover span.download { 189 | background-position: 0 -45px; 190 | } 191 | 192 | a.button.active span.book, a.button.warning span.book, a.button:hover span.book { 193 | background-position: 0 -15px; 194 | } 195 | 196 | div#header a.button { 197 | float: right; 198 | margin-top: 5px; 199 | } 200 | 201 | div#footer { 202 | background-color: #5D5D5D; 203 | background: -moz-linear-gradient(top, #5D5D5D, #4D4D4D); 204 | background: -webkit-linear-gradient(top, #5D5D5D, #4D4D4D); 205 | background: -o-linear-gradient(top, #5D5D5D, #4D4D4D); 206 | color: #ffffff; 207 | text-align: center; 208 | height: 18pt; 209 | padding-top: 3.5pt; 210 | } 211 | 212 | #footerPush { 213 | height: 21pt; 214 | } 215 | 216 | div#footer a, div#footer a:visited, div#footer a:link { 217 | color: #ffffff; 218 | } 219 | 220 | div#footer a:hover { 221 | text-decoration: none; 222 | } 223 | 224 | /* The new platform build result divs */ 225 | 226 | div.platfBuildResult { 227 | width: 32%; 228 | min-width: 350px; 229 | border-radius: 2px 0px 0px 2px; 230 | float: left; 231 | margin-right: 6pt; 232 | margin-bottom: 6pt; 233 | border-right: 2px solid #1d1d1d; 234 | } 235 | 236 | div.platfBuildResult p { 237 | padding-left: 1%; 238 | } 239 | 240 | div.platfBuildResult p.commitMsg { 241 | text-overflow: ellipsis; 242 | white-space: nowrap; 243 | overflow: hidden; 244 | } 245 | 246 | div.platfBuildResult div.header { 247 | background-color: #5D5D5D; 248 | background: -moz-linear-gradient(top, #5D5D5D, #4D4D4D); 249 | background: -webkit-linear-gradient(top, #5D5D5D, #4D4D4D); 250 | background: -o-linear-gradient(top, #5D5D5D, #4D4D4D); 251 | color: white; 252 | padding-left: 5px; 253 | font-weight: bold; 254 | font-size: 10pt; 255 | padding-top: 4px; 256 | padding-bottom: 3px; 257 | border-bottom: 1px solid #3d3d3d; 258 | border-top: 2px solid #3d3d3d; 259 | } 260 | 261 | div.platfBuildResult div.lastResults div.half, div.platfBuildResult div.lastResults div.branch { 262 | margin-bottom: 2px; 263 | } 264 | 265 | div.platfSuccess { 266 | border-left: 7px solid #43B800; 267 | } 268 | 269 | div.platfWarning { 270 | border-left: 7px solid #E87E15; 271 | } 272 | 273 | div.platfProgress { 274 | border-left: 7px solid #108BDE; 275 | } 276 | 277 | div.platfFailure { 278 | border-left: 7px solid #B50707; 279 | } 280 | 281 | div.indivSuccess { 282 | border-top: 3px solid #43B800; 283 | } 284 | 285 | div.indivWarning { 286 | border-top: 3px solid #E87E15; 287 | } 288 | 289 | div.indivFailure { 290 | border-top: 3px solid #B50707; 291 | } 292 | 293 | div.indivUnknown { 294 | border-top: 3px solid #8A8A8A; 295 | } 296 | 297 | 298 | div.platfBuildResult div.half { 299 | width: 22%; 300 | float: left; 301 | padding-left: 1%; 302 | } 303 | 304 | div.platfBuildResult .lastResults { 305 | padding-bottom: 2px; 306 | } 307 | 308 | div.platfBuildResult .lastResults .branch { 309 | width: 53%; 310 | float: left; 311 | padding-left: 1%; 312 | border-top: 3px solid #8A8A8A; 313 | } 314 | 315 | div.platfBuildResult .lastResults .branch span, span.branch { 316 | border-radius: 2px 2px 2px 2px; 317 | color: white; 318 | font-size: 10pt; 319 | padding: 1px 4px 2px 4px; /* Top right bottom left */ 320 | background-color: #0070BF; 321 | cursor: help; 322 | } 323 | 324 | div.platfBuildResult .lastResults .master span, span.master { 325 | background-color: #21A607; 326 | } 327 | 328 | div.platfBuildResult .buildInfo { 329 | padding-top: 5px; 330 | padding-bottom: 5px; 331 | padding-right: 5px; 332 | border-bottom: 2px solid #1d1d1d; 333 | } 334 | 335 | div#resultsWrapper { 336 | width: 100%; 337 | float: left; 338 | } -------------------------------------------------------------------------------- /public/images/download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dom96/nimbuild/303cad7a3b738c987743a00f0ecd72b85d6629c6/public/images/download.png -------------------------------------------------------------------------------- /public/images/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dom96/nimbuild/303cad7a3b738c987743a00f0ecd72b85d6629c6/public/images/error.png -------------------------------------------------------------------------------- /public/images/icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dom96/nimbuild/303cad7a3b738c987743a00f0ecd72b85d6629c6/public/images/icons.png -------------------------------------------------------------------------------- /public/images/progress.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dom96/nimbuild/303cad7a3b738c987743a00f0ecd72b85d6629c6/public/images/progress.gif -------------------------------------------------------------------------------- /public/images/tick.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dom96/nimbuild/303cad7a3b738c987743a00f0ecd72b85d6629c6/public/images/tick.png -------------------------------------------------------------------------------- /readme.markdown: -------------------------------------------------------------------------------- 1 | # Nimbuild 2 | 3 | This is the Nim build farm. It is separated into multiple components; main one being 4 | the website (website.nim); which acts as a ``hub`` for all the other components 5 | - they all connect to it. It also acts as the front end and is available at 6 | http://build.nim-lang.org/. 7 | 8 | The other components: 9 | 10 | ### Github.nim 11 | This component waits for connections from github, it acts as a POST receive hook. 12 | It waits for a POST request from github containing a payload informing it 13 | of the file commited to the Nim repo, it then sends this information on 14 | to the website. 15 | 16 | ### Builder.nim 17 | This component does the actual building, multiple instances of it run on different 18 | platforms. It pulls the latest version of the compiler from github, bootstraps 19 | it, zips the binary then uploads the zip to nimbuild. It then finally runs 20 | the test suite. It also does some other tasks which are optional, like generating 21 | c sources and the documentation. 22 | 23 | ### ircbot.nim 24 | This component is an IRC bot which idles in the #nim channel on freenode, it 25 | has some features already. It's main purpose is to announce a commit in the 26 | channel, but it also has a !seen command. More features are planned for later. 27 | 28 | ## Contributing 29 | Pull requests are always welcome. If you are not much of a programmer you can 30 | always donate a machine, we are always looking for new machines, especially 31 | Windows ones to run nimbuild on, if you have one please contact me on Github 32 | or on freenode (i'm dom96). 33 | -------------------------------------------------------------------------------- /src/builder.nim: -------------------------------------------------------------------------------- 1 | # This will build nimrod using the specified settings. 2 | import 3 | osproc, json, sockets, asyncio, os, streams, parsecfg, parseopt, strutils, 4 | ftpclient, times, strtabs 5 | import types 6 | 7 | const 8 | builderVer = "0.2" 9 | buildReadme = """ 10 | This is a minimal distribution of the Nimrod compiler. Full source code can be 11 | found at http://github.com/Araq/Nimrod 12 | """ 13 | webFP = {fpUserRead, fpUserWrite, fpUserExec, 14 | fpGroupRead, fpGroupExec, fpOthersRead, fpOthersExec} 15 | 16 | type 17 | TJob = object 18 | payload: PJsonNode 19 | p: PProcess ## Current process that is running. 20 | cmd: string 21 | 22 | TCfg = object 23 | nimLoc: string ## Location of the nimrod repo 24 | websiteLoc: string ## Location of the website. 25 | logLoc: string ## Location of the logs for this module. 26 | zipLoc: string ## Location of where to copy the files for zipping. 27 | docgen: bool ## Determines whether to generate docs. 28 | csourceGen: bool ## Determines whether to generate csources. 29 | csourceExtraBuildArgs: string 30 | innoSetupGen: bool 31 | platform: string 32 | hubAddr: string 33 | hubPort: int 34 | hubPass: string 35 | 36 | ftpUser: string 37 | ftpPass: string 38 | ftpPort: TPort 39 | ftpUploadDir: string 40 | 41 | requestNewest: bool 42 | deleteOutgoing: bool 43 | 44 | TState = object of TObject 45 | dispatcher: PDispatcher 46 | sock: PAsyncSocket 47 | building: bool 48 | buildJob: TJob ## Current build 49 | skipCSource: bool ## Skip the process of building csources 50 | logFile: TFile 51 | cfg: TCfg 52 | lastMsgTime: float ## The last time a message was received from the hub. 53 | pinged: float 54 | reconnecting: bool 55 | buildThread: TThread[int] # TODO: Change to void when bug is fixed. 56 | 57 | PState = ref TState 58 | 59 | TBuildProgressType = enum 60 | ProcessStart, ProcessExit, HubMsg, BuildEnd 61 | 62 | TBuildProgress = object ## This object gets sent to the main thread, by the builder thread. 63 | case kind: TBuildProgressType 64 | of ProcessStart: 65 | p: PProcess 66 | of ProcessExit, BuildEnd: nil 67 | of HubMsg: 68 | msg: string 69 | 70 | TThreadCommandType = enum 71 | BuildTerminate, BuildStart 72 | 73 | TThreadCommand = object 74 | case kind: TThreadCommandType 75 | of BuildTerminate: nil 76 | of BuildStart: 77 | payload: PJsonNode 78 | cfg: TCfg 79 | 80 | EBuildEnd = object of ESynch 81 | 82 | var 83 | hubChan: TChannel[TBuildProgress] 84 | threadCommandChan: TChannel[TThreadCommand] 85 | 86 | hubChan.open() 87 | threadCommandChan.open() 88 | 89 | # Configuration 90 | proc parseConfig(state: PState, path: string) = 91 | var f = newFileStream(path, fmRead) 92 | if f != nil: 93 | var p: TCfgParser 94 | open(p, f, path) 95 | var count = 0 96 | while true: 97 | var n = next(p) 98 | case n.kind 99 | of cfgEof: 100 | break 101 | of cfgSectionStart: 102 | raise newException(EInvalidValue, "Unknown section: " & n.section) 103 | of cfgKeyValuePair, cfgOption: 104 | case normalize(n.key) 105 | of "platform": 106 | state.cfg.platform = n.value 107 | inc(count) 108 | if ':' in state.cfg.platform: quit("No ':' allowed in the platform name.") 109 | of "nimgitpath": 110 | state.cfg.nimLoc = n.value 111 | inc(count) 112 | of "websitepath": 113 | state.cfg.websiteLoc = n.value 114 | inc(count) 115 | of "logfilepath": 116 | state.cfg.logLoc = n.value 117 | inc(count) 118 | of "archivepath": 119 | state.cfg.zipLoc = n.value 120 | inc(count) 121 | of "docgen": 122 | state.cfg.docgen = if normalize(n.value) == "true": true else: false 123 | of "csourcegen": 124 | state.cfg.csourceGen = if normalize(n.value) == "true": true else: false 125 | of "innogen": 126 | state.cfg.innoSetupGen = if normalize(n.value) == "true": true else: false 127 | of "csourceextrabuildargs": 128 | state.cfg.csourceExtraBuildArgs = n.value 129 | of "hubaddr": 130 | state.cfg.hubAddr = n.value 131 | inc(count) 132 | of "hubport": 133 | state.cfg.hubPort = parseInt(n.value) 134 | inc(count) 135 | of "hubpass": 136 | state.cfg.hubPass = n.value 137 | of "ftpuser": 138 | state.cfg.ftpUser = n.value 139 | of "ftppass": 140 | state.cfg.ftpPass = n.value 141 | of "ftpport": 142 | state.cfg.ftpPort = parseInt(n.value).TPort 143 | of "ftpuploaddir": 144 | state.cfg.ftpUploadDir = n.value 145 | of "requestnewest": 146 | state.cfg.requestNewest = 147 | if normalize(n.value) == "true": true else: false 148 | of "deleteoutgoing": 149 | state.cfg.deleteOutgoing = 150 | if normalize(n.value) == "true": true else: false 151 | of cfgError: 152 | raise newException(EInvalidValue, "Configuration parse error: " & n.msg) 153 | if count < 7: 154 | quit("Not all settings have been specified in the .ini file", QuitFailure) 155 | if state.cfg.ftpUser != "" and state.cfg.ftpPass == "": 156 | quit("When ftpUser is specified so must the ftpPass.") 157 | 158 | close(p) 159 | else: 160 | quit("Cannot open configuration file: " & path, QuitFailure) 161 | 162 | proc defaultState(): PState = 163 | new(result) 164 | result.cfg.hubAddr = "127.0.0.1" 165 | result.cfg.hubPass = "" 166 | 167 | result.cfg.ftpUser = "" 168 | result.cfg.ftpPass = "" 169 | result.cfg.ftpPort = TPort(21) 170 | 171 | result.lastMsgTime = epochTime() 172 | result.pinged = -1.0 173 | 174 | result.cfg.csourceExtraBuildArgs = "" 175 | 176 | proc initJob(): TJob = 177 | result.payload = nil 178 | 179 | # Build of Nimrod/tests/docs gen 180 | 181 | template sendHubMsg(m: string): stmt = 182 | var bp: TBuildProgress 183 | bp.kind = HubMsg 184 | bp.msg = m 185 | hubChan.send(bp) 186 | 187 | proc hubSendBuildStart(hash, branch: string) = 188 | var obj = %{"eventType": %(int(bStart)), 189 | "hash": %hash, 190 | "branch": %branch} 191 | sendHubMsg($obj & "\c\L") 192 | 193 | proc hubSendProcessStart(process: PProcess, cmd, args: string) = 194 | var bp: TBuildProgress 195 | bp.kind = ProcessStart 196 | bp.p = process 197 | hubChan.send(bp) 198 | var obj = %{"desc": %("\"" & cmd & " " & args & "\" started."), 199 | "eventType": %(int(bProcessStart)), 200 | "cmd": %cmd, 201 | "args": %args} 202 | sendHubMsg($obj & "\c\L") 203 | 204 | proc hubSendProcessLine(line: string) = 205 | var obj = %{"eventType": %(int(bProcessLine)), 206 | "line": %line} 207 | sendHubMsg($obj & "\c\L") 208 | 209 | proc hubSendProcessExit(exitCode: int) = 210 | var bp: TBuildProgress 211 | bp.kind = ProcessExit 212 | hubChan.send(bp) 213 | var obj = %{"eventType": %(int(bProcessExit)), 214 | "exitCode": %exitCode} 215 | sendHubMsg($obj & "\c\L") 216 | 217 | proc hubSendFTPUploadSpeed(speed: float) = 218 | var obj = %{"desc": %("FTP Upload at " & formatFloat(speed) & "KB/s"), 219 | "eventType": %(int(bFTPUploadSpeed)), 220 | "speed": %speed} 221 | sendHubMsg($obj & "\c\L") 222 | 223 | proc hubSendJobUpdate(job: TBuilderJob) = 224 | var obj = %{"job": %(int(job))} 225 | sendHubMsg($obj & "\c\L") 226 | 227 | proc hubSendBuildFail(msg: string) = 228 | var obj = %{"result": %(int(Failure)), 229 | "detail": %msg} 230 | sendHubMsg($obj & "\c\L") 231 | 232 | proc hubSendBuildSuccess() = 233 | var obj = %{"result": %(int(Success))} 234 | sendHubMsg($obj & "\c\L") 235 | 236 | proc hubSendBuildTestSuccess(total, passed, skipped, failed: BiggestInt, 237 | diff, results: PJsonNode) = 238 | var obj = %{"result": %(int(Success)), 239 | "total": %(total), 240 | "passed": %(passed), 241 | "skipped": %(skipped), 242 | "failed": %(failed), 243 | "diff": diff, 244 | "results": results} 245 | sendHubMsg($obj & "\c\L") 246 | 247 | proc hubSendBuildEnd() = 248 | var bp: TBuildProgress 249 | bp.kind = BuildEnd 250 | hubChan.send(bp) 251 | 252 | var obj = %{"eventType": %(int(bEnd))} 253 | sendHubMsg($obj & "\c\L") 254 | 255 | proc dCopyFile(src, dest: string) = 256 | echo("[INFO] Copying ", src, " to ", dest) 257 | copyFile(src, dest) 258 | 259 | proc dMoveFile(src, dest: string) = 260 | echo("[INFO] Moving ", src, " to ", dest) 261 | copyFile(src, dest) 262 | removeFile(src) 263 | 264 | proc dCopyDir(src, dest: string) = 265 | echo("[INFO] Copying directory ", src, " to ", dest) 266 | copyDir(src, dest) 267 | 268 | proc dCreateDir(s: string) = 269 | echo("[INFO] Creating directory ", s) 270 | createDir(s) 271 | 272 | proc dMoveDir(s: string, s1: string) = 273 | echo("[INFO] Moving directory ", s, " to ", s1) 274 | copyDir(s, s1) 275 | removeDir(s) 276 | 277 | proc dRemoveDir(s: string) = 278 | echo("[INFO] Removing directory ", s) 279 | removeDir(s) 280 | 281 | proc dRemoveFile(s: string) = 282 | echo("[INFO] Removing file ", s) 283 | removeFile(s) 284 | 285 | proc copyForArchive(nimLoc, dest, bin: string) = 286 | dCreateDir(dest / "bin") 287 | var nimBin = "bin" / addFileExt(bin, ExeExt) 288 | dCopyFile(nimLoc / nimBin, dest / nimBin) 289 | dCopyFile(nimLoc / "readme.txt", dest / "readme.txt") 290 | dCopyFile(nimLoc / "copying.txt", dest / "copying.txt") 291 | #dCopyFile(nimLoc / "gpl.html", dest / "gpl.html") 292 | writeFile(dest / "readme2.txt", buildReadme) 293 | dCopyDir(nimLoc / "config", dest / "config") 294 | dCopyDir(nimLoc / "lib", dest / "lib") 295 | 296 | proc clearOutgoing(websitePath, platform: string) = 297 | echo("Clearing outgoing folder...") 298 | dRemoveDir(websitePath / "commits" / platform) 299 | dCreateDir(websitePath / "commits" / platform) 300 | 301 | # TODO: Make this a template? 302 | proc tally3(obj: PJsonNode, name: string, 303 | total, passed, skipped: var BiggestInt) = 304 | total = total + obj[name]["total"].num 305 | passed = passed + obj[name]["passed"].num 306 | skipped = skipped + obj[name]["skipped"].num 307 | 308 | proc tallyTestResults(path: string): 309 | tuple[total, passed, skipped, failed: BiggestInt, diff, results: PJsonNode] = 310 | # TODO: Refactor this monstrosity. 311 | var f = readFile(path) 312 | var obj = parseJson(f) 313 | var total: BiggestInt = 0 314 | var passed: BiggestInt = 0 315 | var skipped: BiggestInt = 0 316 | var diff: PJsonNode = newJNull() 317 | var results: PJsonNode = newJNull() 318 | if obj.hasKey("reject") and obj.hasKey("compile") and obj.hasKey("run"): 319 | tally3(obj, "reject", total, passed, skipped) 320 | tally3(obj, "compile", total, passed, skipped) 321 | tally3(obj, "run", total, passed, skipped) 322 | elif obj.hasKey("total") and obj.hasKey("passed") and obj.hasKey("skipped"): 323 | total = obj["total"].num 324 | passed = obj["passed"].num 325 | skipped = obj["skipped"].num 326 | if obj.hasKey("diff"): 327 | diff = obj["diff"] 328 | if obj.hasKey("results"): 329 | results = obj["results"] 330 | else: 331 | raise newException(EBuildEnd, "Invalid testresults.json.") 332 | 333 | return (total, passed, skipped, total - (passed + skipped), diff, results) 334 | 335 | proc fileInModified(json: PJsonNode, file: string): bool = 336 | if json.hasKey("commits"): 337 | for commit in items(json["commits"].elems): 338 | for f in items(commit["modified"].elems): 339 | if f.str == file: return true 340 | 341 | template buildTmpl(infoName: expr, body: stmt): stmt {.immediate.} = 342 | while true: 343 | let thrCmd = threadCommandChan.recv() 344 | case thrCmd.kind: 345 | of BuildTerminate: 346 | echo("[Warning] No bootstrap running.") 347 | of BuildStart: 348 | var infoName = thrCmd 349 | try: 350 | body 351 | except EBuildEnd: 352 | hubSendBuildFail(getCurrentExceptionMsg()) 353 | hubSendBuildEnd() 354 | if info.cfg.deleteOutgoing: 355 | clearOutgoing(info.cfg.websiteLoc, info.cfg.platform) 356 | 357 | proc runProcess(env: PStringTable = nil, workDir, execFile: string, 358 | args: openarray[string]): bool = 359 | ## Returns ``true`` if process finished successfully. Otherwise ``false``. 360 | result = true 361 | var cmd = "" 362 | if isAbsolute(execFile): 363 | cmd = execFile.changeFileExt(ExeExt) 364 | else: 365 | cmd = workDir / execFile.changeFileExt(ExeExt) 366 | var process = startProcess(cmd, workDir, args, env) 367 | hubSendProcessStart(process, execFile.extractFilename, join(args, " ")) 368 | var pStdout = process.outputStream 369 | proc hasProcessTerminated(process: PProcess, exitCode: var int): bool = 370 | result = false 371 | exitCode = process.peekExitCode() 372 | if exitCode != -1: 373 | hubSendProcessExit(exitCode) 374 | return true 375 | var line = "" 376 | var exitCode = -1 377 | while true: 378 | line = "" 379 | if pStdout.readLine(line) and line != "": 380 | hubSendProcessLine(line) 381 | if hasProcessTerminated(process, exitCode): 382 | break 383 | result = exitCode == QuitSuccess 384 | echo("! " & execFile.extractFilename & " " & join(args, " ") & " exited with ", exitCode) 385 | process.close() 386 | 387 | proc changeNimrodInPATH(bindir: string): string = 388 | var paths = getEnv("PATH").split(PathSep) 389 | for i in 0 .. 0: 406 | let thrCmd = threadCommandChan.recv() 407 | case thrCmd.kind: 408 | of BuildTerminate: 409 | raise newException(EBuildEnd, "Bootstrap aborted.") 410 | of BuildStart: 411 | threadCommandChan.send(TThreadCommand(kind: BuildTerminate)) 412 | threadCommandChan.send(thrCmd) 413 | 414 | proc run(workDir: string, exec: string, args: varargs[string]) = 415 | run(nil, workDir, exec, args) 416 | 417 | proc exe(f: string): string = return addFileExt(f, ExeExt) 418 | 419 | proc restoreBranchSpecificBin(dir, bin, branch: string) = 420 | let branchSpecificBin = dir / (bin & "_" & branch).exe 421 | if existsFile(branchSpecificBin): 422 | copyFile(branchSpecificBin, dir / bin.exe) 423 | # Make sure that the binary has +x permissions. 424 | inclFilePermissions(dir / bin.exe, {fpUserExec, fpGroupExec, fpOthersExec}) 425 | elif existsFile(dir / bin.exe): 426 | # Delete the current binary to prevent any issues with old binaries. 427 | removeFile(dir / bin.exe) 428 | 429 | proc backupBranchSpecificBin(dir, bin, branch: string) = 430 | if existsFile(dir / bin.exe): 431 | copyFile(dir / bin.exe, dir / (bin & "_" & branch).exe) 432 | 433 | proc setGIT(payload: PJsonNode, nimLoc: string) = 434 | ## Cleans working tree, changes branch and pulls. 435 | let branch = payload["ref"].str[11 .. ^1] 436 | let commitHash = payload["after"].str 437 | 438 | run(nimLoc, findExe("git"), "checkout", "--", ".") 439 | run(nimLoc, findExe("git"), "fetch", "--all") 440 | run(nimLoc, findExe("git"), "checkout", "-f", "origin/" & branch) 441 | # TODO: Capture changed files from output? 442 | run(nimLoc, findExe("git"), "checkout", commitHash) 443 | 444 | # Determine the nim binary name. Likely 'nim' now. 445 | if existsFile(nimLoc / "compiler" / "nim.nim"): 446 | payload["nimBin"] = %"nim" 447 | else: 448 | payload["nimBin"] = %"nimrod" 449 | 450 | # If a branch specific nimrod binary exists. Change to it. 451 | restoreBranchSpecificBin(nimLoc / "bin", payload["nimBin"].str, branch) 452 | restoreBranchSpecificBin(nimLoc, "koch", branch) 453 | 454 | # Handle C sources 455 | let prevCSourcesHead = 456 | if existsFile(nimLoc / "csources" / ".git" / "refs" / "heads" / branch): 457 | readFile(nimLoc / "csources" / ".git" / "refs" / "heads" / branch) 458 | else: 459 | "" 460 | if existsDir(nimLoc / "csources" / ".git"): 461 | removeDir(nimLoc / "csources") 462 | run(nimLoc, findExe("git"), "clone", "-b", branch, "--depth", "1", 463 | "https://github.com/nimrod-code/csources") 464 | 465 | let currCSourcesHead = readFile(nimLoc / "csources" / ".git" / 466 | "refs" / "heads" / branch) 467 | # Save whether C sources have changed in the payload so that ``nimBootstrap`` 468 | # is aware of it. 469 | payload["csources"] = %(not (prevCSourcesHead == currCSourcesHead)) 470 | 471 | proc clean(nimLoc: string) = 472 | echo "Cleaning up." 473 | proc removePattern(pattern: string) = 474 | for f in walkFiles(pattern): 475 | removeFile(f) 476 | removePattern(nimLoc / "web/*.html") 477 | removePattern(nimLoc / "doc/*.html") 478 | removeFile(nimLoc / "testresults.json") 479 | removeFile(nimLoc / "testresults.html") 480 | 481 | proc nimBootstrap(payload: PJsonNode, nimLoc, csourceExtraBuildArgs: string) = 482 | ## Set of steps to bootstrap Nimrod. In debug and release mode. 483 | ## Does not perform any git actions! 484 | 485 | let nimBin = payload["nimBin"].str 486 | 487 | # skipCSource is already set to true if 'csources.zip' changed. 488 | # force running of ./build.sh if the nimrod binary is nonexistent. 489 | if payload["csources"].bval or 490 | not existsFile(nimLoc / "bin" / nimBin.exe): 491 | clean(nimLoc) 492 | 493 | # Unzip C Sources 494 | when defined(windows): 495 | # build.bat 496 | run(nimLoc / "csources", getEnv("COMSPEC"), "/c", "build.bat", csourceExtraBuildArgs) 497 | else: 498 | # ./build.sh 499 | run(nimLoc / "csources", findExe("sh"), "build.sh", csourceExtraBuildArgs) 500 | 501 | if (not existsFile(nimLoc / "koch".exe)) or 502 | fileInModified(payload, "koch.nim"): 503 | run(nimLoc, "bin" / nimBin.exe, "c", "koch.nim") 504 | backupBranchSpecificBin(nimLoc, "koch", payload["ref"].str[11 .. ^1]) 505 | 506 | # Bootstrap! 507 | run(nimLoc, "koch".exe, "boot") 508 | run(nimLoc, "koch".exe, "boot", "-d:release") 509 | backupBranchSpecificBin(nimLoc / "bin", nimBin, payload["ref"].str[11 .. ^1]) 510 | 511 | proc archiveNimrod(platform, commitPath, commitHash, websiteLoc, 512 | nimLoc, nimBin, rootZipLoc: string): string = 513 | ## Zips up the build. 514 | ## Returns the full absolute path to where the zipped file resides. 515 | 516 | # Set +x on nimrod binary 517 | setFilePermissions(nimLoc / "bin" / nimBin.exe, webFP) 518 | let zipPath = rootZipLoc / commitPath 519 | let zipFile = addFileExt(commitPath, "zip") 520 | 521 | dCreateDir(zipPath) 522 | copyForArchive(nimLoc, zipPath, nimBin) 523 | 524 | # Remove the .zip in case it already exists... 525 | if existsFile(rootZipLoc / zipFile): removeFile(rootZipLoc / zipFile) 526 | when defined(windows): 527 | run(rootZipLoc, findExe("7za"), "a", "-tzip", 528 | zipFile.extractFilename, commitPath) 529 | else: 530 | run(rootZipLoc, findExe("zip"), "-r", zipFile, commitPath) 531 | 532 | # Copy the .zip file 533 | var zipFinalPath = addFileExt(makeZipPath(platform, commitHash), "zip") 534 | # Remove the pre-zipped folder with the binaries. 535 | dRemoveDir(zipPath) 536 | # Move the .zip file to the website 537 | when defined(windows): 538 | dMoveFile(rootZipLoc / zipFile.extractFilename, 539 | websiteLoc / "commits" / zipFinalPath) 540 | else: 541 | dMoveFile(rootZipLoc / zipFile, websiteLoc / "commits" / zipFinalPath) 542 | # Remove the original .zip file 543 | dRemoveFile(rootZipLoc / zipFile) 544 | 545 | result = websiteLoc / "commits" / zipFinalPath 546 | 547 | proc uploadFile(ftpAddr: string, ftpPort: TPort, user, pass, workDir, 548 | uploadDir, file, destFile: string) = 549 | 550 | proc handleEvent(f: PAsyncFTPClient, ev: TFTPEvent) = 551 | case ev.typ 552 | of EvStore: 553 | f.chmod(destFile, webFP) 554 | f.close() 555 | of EvTransferProgress: 556 | hubSendFTPUploadSpeed(ev.speed.float / 1024.0) 557 | else: assert false 558 | 559 | try: 560 | var ftpc = asyncFTPClient(ftpAddr, ftpPort, user, pass, handleEvent) 561 | echo("Connecting to ftp://" & user & "@" & ftpAddr & ":" & $ftpPort) 562 | ftpc.connect() 563 | assert ftpc.pwd().startsWith("/home/" & user) # /home/nimrod 564 | ftpc.cd(workDir) 565 | echo("FTP: Work dir is " & workDir) 566 | echo("FTP: Creating " & uploadDir) 567 | try: ftpc.createDir(uploadDir, true) 568 | except EInvalidReply: nil # TODO: Check properly whether the folder exists 569 | 570 | ftpc.chmod(uploadDir, webFP) 571 | ftpc.cd(uploadDir) 572 | echo("FTP: Work dir is " & ftpc.pwd()) 573 | var disp = newDispatcher() 574 | disp.register(ftpc) 575 | echo("FTP: Uploading ", file, " to ", destFile) 576 | ftpc.store(file, destFile, async = true) 577 | while true: 578 | if not disp.poll(5000): break 579 | 580 | except EInvalidReply: raise newException(EBuildEnd, getCurrentExceptionMsg()) 581 | 582 | proc nimTest(commitPath, nimLoc, websiteLoc: string): string = 583 | ## Runs the tester, returns the full absolute path to where the tests 584 | ## have been saved. 585 | result = websiteLoc / "commits" / commitPath / "testresults.html" 586 | run({"PATH": changeNimrodInPATH(nimLoc / "bin")}.newStringTable(), 587 | nimLoc, "koch".exe, "tests") 588 | # Copy the testresults.html file. 589 | dCreateDir(websiteLoc / "commits" / commitPath) 590 | setFilePermissions(websiteLoc / "commits" / commitPath, 591 | webFP) 592 | dCopyFile(nimLoc / "testresults.html", result) 593 | 594 | proc bootstrapTmpl(dummy: int) {.thread.} = 595 | ## Template for a full bootstrap. 596 | buildTmpl(info): 597 | let cfg = info.cfg 598 | let commitHash = info.payload["after"].str 599 | let commitBranch = info.payload["ref"].str[11 .. ^1] 600 | let commitPath = makeCommitPath(cfg.platform, commitHash) 601 | hubSendBuildStart(commitHash, commitBranch) 602 | hubSendJobUpdate(jBuild) 603 | 604 | # GIT 605 | setGIT(info.payload, cfg.nimLoc) 606 | 607 | # Bootstrap 608 | nimBootstrap(info.payload, cfg.nimLoc, cfg.csourceExtraBuildArgs) 609 | 610 | var buildZipFilePath = archiveNimrod(cfg.platform, commitPath, commitHash, 611 | cfg.websiteLoc, cfg.nimLoc, info.payload["nimBin"].str, cfg.zipLoc) 612 | 613 | # --- Upload zip with build --- 614 | if cfg.hubAddr != "127.0.0.1": 615 | uploadFile(cfg.hubAddr, cfg.ftpPort, cfg.ftpUser, 616 | cfg.ftpPass, 617 | cfg.ftpUploadDir / "commits", cfg.platform, # TODO: Make sure user doesn't add the "commits" in the config. 618 | buildZipFilePath, 619 | buildZipFilePath.extractFilename) 620 | 621 | hubSendBuildSuccess() 622 | hubSendJobUpdate(jTest) 623 | var testResultsPath = nimTest(commitPath, cfg.nimLoc, cfg.websiteLoc) 624 | 625 | # --- Upload testresults.html --- 626 | if cfg.hubAddr != "127.0.0.1": 627 | uploadFile(cfg.hubAddr, cfg.ftpPort, cfg.ftpUser, 628 | cfg.ftpPass, cfg.ftpUploadDir / "commits", commitPath, 629 | testResultsPath, "testresults.html") 630 | var (total, passed, skipped, failed, diff, results) = 631 | tallyTestResults(cfg.nimLoc / "testresults.json") 632 | hubSendBuildTestSuccess(total, passed, skipped, failed, diff, results) 633 | 634 | # --- Start of doc gen --- 635 | # Create the upload directory and the docs directory on the website 636 | if cfg.docgen: 637 | hubSendJobUpdate(jDocGen) 638 | dCreateDir(cfg.nimLoc / "web" / "upload") 639 | dCreateDir(cfg.websiteLoc / "docs") 640 | run({"PATH": changeNimrodInPATH(cfg.nimLoc / "bin")}.newStringTable(), 641 | cfg.nimLoc, "koch", "web") 642 | # Copy all the docs to the website. 643 | dCopyDir(cfg.nimLoc / "web" / "upload", cfg.websiteLoc / "docs") 644 | 645 | hubSendBuildSuccess() 646 | if cfg.innoSetupGen: 647 | # We want docs to be generated for inno setup, so that the setup file 648 | # includes them. 649 | hubSendJobUpdate(jDocGen) 650 | run({"PATH": changeNimrodInPATH(cfg.nimLoc / "bin")}.newStringTable(), 651 | cfg.nimLoc, "koch", "web") 652 | hubSendBuildSuccess() 653 | 654 | 655 | # --- Start of csources gen --- 656 | if cfg.csourceGen: 657 | # Rename the build directory so that the csources from the git repo aren't 658 | # overwritten 659 | hubSendJobUpdate(jCSrcGen) 660 | dMoveDir(cfg.nimLoc / "build", cfg.nimLoc / "build_old") 661 | dCreateDir(cfg.nimLoc / "build") 662 | 663 | run({"PATH": changeNimrodInPATH(cfg.nimLoc / "bin")}.newStringTable(), 664 | cfg.nimLoc, "koch", "csource") 665 | 666 | # Zip up the csources. 667 | # -- Move the build directory to the zip location 668 | let csourcesPath = makeZipPath(cfg.platform, commitHash) & "_csources" 669 | var csourcesZipFile = csourcesPath.addFileExt("zip") 670 | dMoveDir(cfg.nimLoc / "build", cfg.zipLoc / csourcesPath) 671 | # -- Move `build_old` to where it was previously. 672 | dMoveDir(cfg.nimLoc / "build_old", cfg.nimLoc / "build") 673 | # -- License 674 | dCopyFile(cfg.nimLoc / "copying.txt", 675 | cfg.zipLoc / csourcesPath / "copying.txt") 676 | 677 | writeFile(cfg.zipLoc / csourcesPath / "readme2.txt", buildReadme) 678 | # -- ZIP! 679 | if existsFile(cfg.zipLoc / csourcesZipFile): 680 | removeFile(cfg.zipLoc / csourcesZipFile) 681 | when defined(windows): 682 | echo("Not implemented") 683 | doAssert(false) 684 | run(cfg.zipLoc, findexe("zip"), "-r", csourcesZipFile, csourcesPath) 685 | # -- Remove the directory which was zipped 686 | dRemoveDir(cfg.zipLoc / csourcesPath) 687 | # -- Move the .zip file 688 | dMoveFile(cfg.zipLoc / csourcesZipFile, 689 | cfg.websiteLoc / "commits" / csourcesZipFile) 690 | 691 | hubSendBuildSuccess() 692 | 693 | # --- Start of inno setup gen --- 694 | if cfg.innoSetupGen: 695 | hubSendJobUpdate(jInnoSetup) 696 | run({"PATH": changeNimrodInPATH(cfg.nimLoc / "bin")}.newStringTable(), 697 | cfg.nimLoc, "koch", "inno", "-d:release") 698 | if cfg.hubAddr != "127.0.0.1": 699 | uploadFile(cfg.hubAddr, cfg.ftpPort, cfg.ftpUser, 700 | cfg.ftpPass, cfg.ftpUploadDir / "commits", cfg.platform, 701 | cfg.nimLoc / "build" / "nimrod_setup.exe", 702 | makeInnoSetupPath(commitHash)) 703 | hubSendBuildSuccess() 704 | 705 | proc stopBuild(state: PState) = 706 | ## Terminates a build 707 | # TODO: Send a message to the website, make it record it to the database 708 | # as "terminated". 709 | if state.building: 710 | # Send the termination command first. 711 | threadCommandChan.send(TThreadCommand(kind: BuildTerminate)) 712 | 713 | # Simply terminate the currently running process, should hopefully work. 714 | if state.buildJob.p != nil: 715 | echo("Terminating build") 716 | state.buildJob.p.terminate() 717 | 718 | proc beginBuild(state: PState) = 719 | ## This procedure starts the process of building nimrod. 720 | 721 | # First make sure to stop any currently running process. 722 | state.stopBuild() 723 | 724 | # Tell the thread to start a build. 725 | state.building = true 726 | let thrCmd = TThreadCommand(kind: BuildStart, 727 | payload: state.buildJob.payload, cfg: state.cfg) 728 | threadCommandChan.send(thrCmd) 729 | 730 | proc pollBuild(state: PState) = 731 | ## This is called from the main loop; it checks whether the bootstrap 732 | ## thread has sent any messages through the channel and it then processes 733 | ## the messages. 734 | let msgCount = hubChan.peek() 735 | if msgCount > 0: 736 | for i in 0..msgCount-1: 737 | var msg = hubChan.recv() 738 | case msg.kind 739 | of ProcessStart: 740 | #p: PProcess 741 | state.buildJob.p = msg.p 742 | of ProcessExit: 743 | state.buildJob.p = nil 744 | of HubMsg: 745 | state.sock.send(msg.msg) 746 | of BuildEnd: 747 | state.building = false 748 | 749 | # Communication 750 | proc parseReply(line: string, expect: string): bool = 751 | var jsonDoc = parseJson(line) 752 | return jsonDoc["reply"].str == expect 753 | 754 | proc hubConnect(state: PState, reconnect: bool) {.gcsafe.} 755 | proc handleConnect(s: PAsyncSocket, state: PState) {.gcsafe.} = 756 | try: 757 | # Send greeting 758 | var obj = newJObject() 759 | obj["name"] = newJString("builder") 760 | obj["platform"] = newJString(state.cfg.platform) 761 | obj["version"] = %"1" 762 | if state.cfg.hubPass != "": obj["pass"] = newJString(state.cfg.hubPass) 763 | state.sock.send($obj & "\c\L") 764 | # Wait for reply. 765 | var readSocks = @[state.sock.getSocket] 766 | # TODO: Don't use select here. Just use readLine with a timeout. 767 | if select(readSocks, 1500) == 1: 768 | var line = "" 769 | if not state.sock.readLine(line): 770 | raise newException(EInvalidValue, "recvLine failed.") 771 | if not parseReply(line, "OK"): 772 | raise newException(EInvalidValue, "Incorrect welcome message from hub") 773 | 774 | echo("The hub accepted me!") 775 | 776 | if state.cfg.requestNewest and not state.reconnecting: 777 | echo("Requesting newest commit.") 778 | var req = newJObject() 779 | req["latestCommit"] = newJNull() 780 | state.sock.send($req & "\c\L") 781 | 782 | else: 783 | raise newException(EInvalidValue, 784 | "Hub didn't accept me. Waited 1.5 seconds.") 785 | except EOS, EInvalidValue: 786 | echo(getCurrentExceptionMsg()) 787 | s.close() 788 | echo("Waiting 5 seconds...") 789 | sleep(5000) 790 | try: hubConnect(state, true) except EOS: echo(getCurrentExceptionMsg()) 791 | 792 | proc handleHubMessage(s: PAsyncSocket, state: PState) {.gcsafe.} 793 | proc hubConnect(state: PState, reconnect: bool) = 794 | state.sock = asyncSocket() 795 | state.sock.handleConnect = 796 | proc (s: PAsyncSocket) {.gcsafe.} = 797 | handleConnect(s, state) 798 | state.sock.handleRead = 799 | proc (s: PAsyncSocket) {.gcsafe.} = 800 | handleHubMessage(s, state) 801 | state.reconnecting = reconnect 802 | state.sock.connect(state.cfg.hubAddr, TPort(state.cfg.hubPort)) 803 | state.dispatcher.register(state.sock) 804 | 805 | proc open(configPath: string): PState = 806 | var cres: PState 807 | cres = defaultState() 808 | # Get config 809 | parseConfig(cres, configPath) 810 | if not existsDir(cres.cfg.nimLoc): 811 | quit(cres.cfg.nimLoc & " does not exist!", QuitFailure) 812 | 813 | # Init dispatcher 814 | cres.dispatcher = newDispatcher() 815 | 816 | # Connect to the hub 817 | try: cres.hubConnect(false) 818 | except EOS: 819 | echo("Could not connect to hub: " & getCurrentExceptionMsg()) 820 | quit(QuitFailure) 821 | 822 | # Open log file 823 | cres.logFile = open(cres.cfg.logLoc, fmAppend) 824 | 825 | # Init job 826 | cres.buildJob = initJob() 827 | 828 | # Start build thread 829 | createThread(cres.buildThread, bootstrapTmpl, 0) 830 | 831 | result = cres 832 | 833 | proc initJob(payload: PJsonNode): TJob = 834 | result.payload = payload 835 | 836 | proc hubDisconnect(state: PState) = 837 | state.sock.close() 838 | 839 | state.lastMsgTime = epochTime() 840 | state.pinged = -1.0 841 | 842 | proc parseMessage(state: PState, line: string) = 843 | echo("Got message from hub: ", line) 844 | state.lastMsgTime = epochTime() 845 | var json = parseJson(line) 846 | if json.hasKey("payload"): 847 | if json["rebuild"].bval: 848 | # This commit has already been built. We don't get a full payload as 849 | # it is not stored. 850 | # Because the build process depends on "after" that is all that is 851 | # needed. 852 | assert(json["payload"].hasKey("after")) 853 | state.buildJob = initJob(json["payload"]) 854 | echo("Re-bootstrapping!") 855 | state.beginBuild() 856 | else: 857 | # This should be a message from the "github" module 858 | # The payload object should have a `after` string. 859 | assert(json["payload"].hasKey("after")) 860 | state.buildJob = initJob(json["payload"]) 861 | echo("Bootstrapping!") 862 | state.beginBuild() 863 | 864 | elif json.hasKey("ping"): 865 | # Website is making sure that the connection is alive. 866 | # All we do is change the "ping" to "pong" and reply. 867 | json["pong"] = json["ping"] 868 | json.delete("ping") 869 | state.sock.send($json & "\c\L") 870 | echo("Replying to Ping") 871 | 872 | elif json.hasKey("pong"): 873 | # Website replied. Connection is still alive. 874 | state.pinged = -1.0 875 | echo("Hub replied to PING. Still connected") 876 | 877 | elif json.hasKey("fatal"): 878 | # Fatal error occurred in the website. We must exit. 879 | echo("FATAL ERROR") 880 | echo(json["fatal"]) 881 | hubDisconnect(state) 882 | quit(QuitFailure) 883 | 884 | elif json.hasKey("do"): 885 | case json["do"].str 886 | of "stop": 887 | ## Terminate build 888 | state.stopBuild() 889 | else: 890 | echo("[FATAL] Don't understand message from hub") 891 | assert false 892 | 893 | proc reconnect(state: PState) = 894 | state.hubDisconnect() 895 | echo("Waiting 5 seconds before reconnecting...") 896 | sleep(5000) 897 | try: state.hubConnect(true) 898 | except EOS: 899 | echo("Could not reconnect: ", getCurrentExceptionMsg()) 900 | reconnect(state) 901 | 902 | proc handleHubMessage(s: PAsyncSocket, state: PState) = 903 | try: 904 | var line = "" 905 | if state.sock.readLine(line): 906 | if line != "": 907 | state.parseMessage(line) 908 | else: 909 | echo("Disconnected from hub (recvLine returned \"\"): ", 910 | osErrorMsg(osLastError())) 911 | reconnect(state) 912 | except EOS: 913 | echo("Disconnected from hub: ", getCurrentExceptionMsg()) 914 | reconnect(state) 915 | 916 | proc checkTimeout(state: PState) = 917 | const timeoutSeconds = 110.0 # If no message received in that long, ping the server. 918 | 919 | if state.cfg.hubAddr != "127.0.0.1": 920 | # Check how long ago the last message was sent. 921 | if state.pinged == -1.0: 922 | if epochTime() - state.lastMsgTime >= timeoutSeconds: 923 | echo("We seem to be timing out! PINGing server.") 924 | var jsonObject = newJObject() 925 | jsonObject["ping"] = newJString(formatFloat(epochTime())) 926 | try: 927 | state.sock.send($jsonObject & "\c\L") 928 | except EOS: 929 | echo("Disconnected from server due to: ", getCurrentExceptionMsg()) 930 | reconnect(state) 931 | return 932 | 933 | state.pinged = epochTime() 934 | 935 | else: 936 | if epochTime() - state.pinged >= 5.0: # 5 seconds 937 | echo("Server has not replied with a pong in 5 seconds.") 938 | # TODO: What happens if the builder gets disconnected in the middle of a 939 | # build? Maybe implement restoration of that. 940 | reconnect(state) 941 | 942 | proc showHelp() = 943 | const help = """Usage: builder [options] configFile 944 | -h --help Show this help message 945 | -v --version Show version 946 | """ 947 | quit(help, QuitSuccess) 948 | 949 | proc showVersion() = 950 | const version = """builder $1 - built on $2 951 | This software is part of the nimbuild website.""" 952 | quit(version % [builderVer, CompileDate & " " & CompileTime], QuitSuccess) 953 | 954 | proc parseArgs(): string = 955 | result = "" 956 | for kind, key, val in getopt(): 957 | case kind 958 | of cmdArgument: 959 | result = key 960 | of cmdLongOption, cmdShortOption: 961 | case key 962 | of "help", "h": showHelp() 963 | of "version", "v": showVersion() 964 | of cmdEnd: assert(false) # cannot happen 965 | if result == "": 966 | showHelp() 967 | 968 | proc createFolders(state: PState) = 969 | if not existsDir(state.cfg.websiteLoc / "commits" / state.cfg.platform): 970 | dCreateDir(state.cfg.websiteLoc / "commits" / state.cfg.platform) 971 | 972 | proc checkDepends() = 973 | when defined(windows): 974 | if findExe("7za") == "": 975 | quit("Could not find 7za for archiving.") 976 | else: 977 | if findExe("zip") == "": 978 | quit("Could not find zip for archiving.") 979 | 980 | when isMainModule: 981 | echo("Started builder: built at ", CompileDate, " ", CompileTime) 982 | 983 | var state = builder.open(parseArgs()) 984 | checkDepends() 985 | createFolders(state) 986 | while true: 987 | discard state.dispatcher.poll() 988 | 989 | state.pollBuild() 990 | 991 | state.checkTimeout() 992 | 993 | 994 | -------------------------------------------------------------------------------- /src/builder.nim.cfg: -------------------------------------------------------------------------------- 1 | threads:on 2 | -d:debug 3 | @if windows: 4 | tlsEmulation:on 5 | @end 6 | @if bsd: 7 | tlsEmulation:on 8 | -d:useFork 9 | @end 10 | -------------------------------------------------------------------------------- /src/db.nim: -------------------------------------------------------------------------------- 1 | # This module is used by the website. 2 | import redis, times, strutils 3 | from sockets import TPort 4 | 5 | type 6 | TDb* = object 7 | r*: TRedis 8 | lastPing: float 9 | 10 | TBuildResult* = enum 11 | bUnknown, bFail, bSuccess 12 | 13 | TTestResult* = enum 14 | tUnknown, tFail, tSuccess 15 | 16 | TEntry* = tuple[c: TCommit, p: seq[TPlatform]] 17 | 18 | TCommit* = object 19 | commitMsg*, username*, hash*, branch*: string 20 | date*: TTime 21 | 22 | # TODO: rename to TBuild? 23 | TPlatform* = object 24 | buildResult*: TBuildResult 25 | testResult*: TTestResult 26 | failReason*, platform*: string 27 | total*, passed*, skipped*, failed*: BiggestInt 28 | csources*: bool 29 | docs*: bool 30 | 31 | const 32 | listName = "commits" 33 | failOnExisting = false 34 | 35 | proc open*(host = "localhost", port: TPort): TDb = 36 | result.r = redis.open(host, port) 37 | result.lastPing = epochTime() 38 | 39 | proc customHSet(database: TDb, name, field, value: string) = 40 | if database.r.hSet(name, field, value).int == 0: 41 | if failOnExisting: 42 | assert(false) 43 | else: 44 | echo("[Warning:REDIS] ", field, " already exists in ", name) 45 | 46 | proc updateProperty*(database: TDb, commitHash, platform, property, 47 | value: string) = 48 | var name = platform & ":" & commitHash 49 | if database.r.hSet(name, property, value).int == 0: 50 | echo("[INFO:REDIS] '$1' field updated in hash" % [property]) 51 | else: 52 | echo("[INFO:REDIS] '$1' new field added to hash" % [property]) 53 | 54 | proc globalProperty*(database: TDb, commitHash, property, value: string) = 55 | if database.r.hSet(commitHash, property, value).int == 0: 56 | echo("[INFO:REDIS] '$1' field updated in hash" % [property]) 57 | else: 58 | echo("[INFO:REDIS] '$1' new field added to hash" % [property]) 59 | 60 | proc addCommit*(database: TDb, commitHash, commitMsg, user, branch: string) = 61 | # Add the commit hash to the `commits` list. 62 | discard database.r.lPush(listName, commitHash) 63 | # Add the commit message, current date and username as a property 64 | globalProperty(database, commitHash, "commitMsg", commitMsg) 65 | globalProperty(database, commitHash, "date", $int(getTime())) 66 | globalProperty(database, commitHash, "username", user) 67 | globalProperty(database, commitHash, "branch", branch) 68 | 69 | proc keepAlive*(database: var TDb) = 70 | ## Keep the connection alive. Ping redis in this case. This functions does 71 | ## not guarantee that redis will be pinged. 72 | var t = epochTime() 73 | if t - database.lastPing >= 60.0: 74 | echo("PING -> redis") 75 | assert(database.r.ping() == "PONG") 76 | database.lastPing = t 77 | 78 | proc getCommits*(database: TDb, 79 | plStr: var seq[string]): seq[TEntry] = 80 | result = @[] 81 | var commitsRaw = database.r.lrange("commits", 0, -1) 82 | for c in items(commitsRaw): 83 | var commit: TCommit 84 | commit.hash = c 85 | for key, value in database.r.hPairs(c): 86 | case normalize(key) 87 | of "commitmsg": commit.commitMsg = value 88 | of "date": commit.date = TTime(parseInt(value)) 89 | of "username": commit.username = value 90 | of "branch": commit.branch = value 91 | else: 92 | echo("[redis] Key not found: ", key) 93 | assert(false) 94 | 95 | var platformsRaw = database.r.lrange(c & ":platforms", 0, -1) 96 | var platforms: seq[TPlatform] = @[] 97 | for p in items(platformsRaw): 98 | var platform: TPlatform 99 | for key, value in database.r.hPairs(p & ":" & c): 100 | case normalize(key) 101 | of "buildresult": 102 | platform.buildResult = parseInt(value).TBuildResult 103 | of "testresult": 104 | platform.testResult = parseInt(value).TTestResult 105 | of "failreason": 106 | platform.failReason = value 107 | of "total": 108 | platform.total = parseBiggestInt(value) 109 | of "passed": 110 | platform.passed = parseBiggestInt(value) 111 | of "skipped": 112 | platform.skipped = parseBiggestInt(value) 113 | of "failed": 114 | platform.failed = parseBiggestInt(value) 115 | of "csources": 116 | platform.csources = if value == "t": true else: false 117 | of "docs": 118 | platform.docs = if value == "t": true else: false 119 | else: 120 | echo("[redis] platf key not found: " & normalize(key)) 121 | assert(false) 122 | 123 | platform.platform = p 124 | 125 | platforms.add(platform) 126 | if p notin plStr: 127 | plStr.add(p) 128 | result.add((commit, platforms)) 129 | 130 | proc commitExists*(database: TDb, commit: string, starts = false): bool = 131 | # TODO: Consider making the 'commits' list a set. 132 | for c in items(database.r.lrange("commits", 0, -1)): 133 | if starts: 134 | if c.startsWith(commit): return true 135 | else: 136 | if c == commit: return true 137 | return false 138 | 139 | proc platformExists*(database: TDb, commit: string, platform: string): bool = 140 | for p in items(database.r.lrange(commit & ":" & "platforms", 0, -1)): 141 | if p == platform: return true 142 | 143 | proc expandHash*(database: TDb, commit: string): string = 144 | for c in items(database.r.lrange("commits", 0, -1)): 145 | if c.startsWith(commit): return c 146 | assert false 147 | 148 | proc isNewest*(database: TDb, commit: string): bool = 149 | return database.r.lIndex("commits", 0) == commit 150 | 151 | proc getNewest*(database: TDb): string = 152 | return database.r.lIndex("commits", 0) 153 | 154 | proc getBranch*(database: TDb, commit: string): string = 155 | if database.r.hExists(commit, "branch"): 156 | return database.r.hGet(commit, "branch") 157 | else: 158 | return "master" 159 | 160 | proc addPlatform*(database: TDb, commit: string, platform: string) = 161 | assert database.commitExists(commit) 162 | assert (not database.platformExists(commit, platform)) 163 | var name = platform & ":" & commit 164 | if database.r.exists(name): 165 | if failOnExisting: quit("[FAIL] " & name & " already exists!", 1) 166 | else: echo("[Warning] " & name & " already exists!") 167 | 168 | discard database.r.lPush(commit & ":" & "platforms", platform) 169 | 170 | proc `[]`*(p: seq[TPlatform], name: string): TPlatform = 171 | for platform in items(p): 172 | if platform.platform == name: 173 | return platform 174 | raise newException(EInvalidValue, name & " platforms not found in commits.") 175 | 176 | proc contains*(p: seq[TPlatform], s: string): bool = 177 | for i in items(p): 178 | if i.platform == s: 179 | return true 180 | 181 | -------------------------------------------------------------------------------- /src/github.nim: -------------------------------------------------------------------------------- 1 | import strtabs, sockets, scgi, strutils, os, json, 2 | osproc, streams, times, parseopt, parseutils 3 | 4 | from cgi import URLDecode 5 | from httpclient import get # httpclient.post conflicts with jester.post 6 | from net import nil 7 | 8 | import asyncio 9 | import asyncdispatch except Port, newDispatcher 10 | 11 | import jester 12 | import types 13 | 14 | 15 | type 16 | TSubnet = object 17 | cidr: range[8 .. 32] 18 | a, b, c, d: int 19 | 20 | PState = ref TState 21 | TState = object of TObject 22 | dispatcher: PDispatcher 23 | sock: PAsyncSocket 24 | scgi: PAsyncScgiState 25 | platform: string 26 | 27 | hubPort: TPort 28 | scgiPort: net.Port 29 | 30 | timeReconnected: float 31 | 32 | subnets: seq[TSubnet] # TODO: Separate into a TGithubAPI object. 33 | apiETag: string 34 | lastAPIAccess: float 35 | 36 | when not defined(ssl): 37 | {.error: "Need SSL support to get Github's IPs, compile with -d:ssl.".} 38 | 39 | # Command line reading 40 | proc getCommandArgs(state: PState) = 41 | for kind, key, value in getOpt(): 42 | case kind 43 | of cmdArgument: 44 | quit("Syntax: ./github --hp:hubPort --sp:scgiPort") 45 | of cmdLongOption, cmdShortOption: 46 | if value == "": 47 | quit("Syntax: ./github --hp:hubPort --sp:scgiPort") 48 | case key 49 | of "hubPort", "hp": 50 | state.hubPort = TPort(parseInt(value)) 51 | of "scgiPort", "sp": 52 | state.scgiPort = net.Port(parseInt(value)) 53 | else: quit("Syntax: ./github -hp hubPort -sp scgiPort") 54 | of cmdEnd: assert false 55 | 56 | # Github specific 57 | 58 | # -- subnets 59 | 60 | proc invalidSubnet(msg: string = "Invalid subnet") = 61 | raise newException(EInvalidValue, msg) 62 | 63 | proc parseSubnet(subnet: string): TSubnet = 64 | var i = 0 65 | 66 | template parsePart(letter: expr, dot: bool) = 67 | var j = parseInt(subnet, letter, i) 68 | if j <= 0: invalidSubnet() 69 | inc(i, j) 70 | if dot: 71 | if subnet[i] == '.': inc(i) 72 | else: invalidSubnet("Invalid subnet, expected '.'.") 73 | 74 | parsePart(result.a, true) 75 | parsePart(result.b, true) 76 | parsePart(result.c, true) 77 | parsePart(result.d, false) 78 | # Parse CIDR 79 | if subnet[i] != '/': invalidSubnet("Invalid subnet, expected '/'.") 80 | inc(i) 81 | var cidr = 0 82 | let j = parseInt(subnet, cidr, i) 83 | if j <= 0: invalidSubnet("Invalid subnet, expected int after '/'.") 84 | inc(i, j) 85 | if subnet[i] != '\0': invalidSubnet("Invalid subnet, expected \0.") 86 | result.cidr = cidr 87 | 88 | proc calcSubmask(cidr: range[8 .. 32]): int = 89 | for i in 0 .. int(cidr)-1: 90 | result = 1 shl (i+(32-cidr)) or result 91 | 92 | proc contains(subnet: TSubnet, ip: string): bool = 93 | # http://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing#CIDR_blocks 94 | let submask = (not calcSubmask(subnet.cidr)) and 0xFFFFFFFF # Mask to 32bits 95 | let subnetIP = subnet.a shl 24 or subnet.b shl 16 or 96 | subnet.c shl 8 or subnet.d 97 | let ipmask = parseIP4(ip) 98 | result = (subnetIP or submask) == (ipmask or submask) 99 | 100 | proc getHookSubnets(state: PState, timeout = 3000) = 101 | ## Gets the allowed IP addresses using Github's API. 102 | except: echo(" [Warning] Getting hookSubnets failed: ", getCurrentExceptionMsg()) 103 | 104 | if epochTime() - state.lastAPIAccess < 5.0: 105 | return 106 | 107 | var extraHeaders = 108 | if state.apiETag != "": "If-None-Match: \"" & state.apiETag & "\"\c\L" 109 | else: "" 110 | let resp = httpclient.get("https://api.github.com/meta", extraHeaders) 111 | state.lastAPIAccess = epochTime() 112 | if resp.status[0] in {'4', '5'}: 113 | echo(" [Warning] HookSubnets won't change. Status code was: ", resp.status) 114 | return 115 | elif resp.status[0 .. 2] == "304": 116 | # Nothing changed. 117 | return 118 | 119 | let j = parseJSON(resp.body) 120 | if j.existsKey("hooks"): 121 | for ip in j["hooks"]: state.subnets.add(parseSubnet(ip.str)) 122 | 123 | state.apiETag = resp.headers["ETag"] 124 | 125 | # Communication 126 | 127 | proc parseReply(line: string, expect: string): bool = 128 | var jsonDoc = parseJson(line) 129 | return jsonDoc["reply"].str == expect 130 | 131 | proc hubConnect(state: PState) 132 | proc handleConnect(s: PAsyncSocket, state: PState) = 133 | try: 134 | # Send greeting 135 | var obj = newJObject() 136 | obj["name"] = newJString("github") 137 | obj["platform"] = newJString(state.platform) 138 | obj["version"] = %"1" 139 | state.sock.send($obj & "\c\L") 140 | # Wait for reply. 141 | var line = "" 142 | sleep(1500) 143 | if state.sock.readLine(line): 144 | assert(line != "") 145 | doAssert parseReply(line, "OK") 146 | echo("The hub accepted me!") 147 | else: 148 | raise newException(EInvalidValue, 149 | "Hub didn't accept me. Waited 1.5 seconds.") 150 | except EOS: 151 | echo(getCurrentExceptionMsg()) 152 | s.close() 153 | echo("Waiting 5 seconds.") 154 | sleep(5000) 155 | state.hubConnect() 156 | 157 | proc handleMessage(state: PState, line: string) = 158 | echo("Got message from hub: ", line) 159 | 160 | proc handleModuleMessage(s: PAsyncSocket, state: PState) = 161 | var line = "" 162 | if not state.sock.readLine(line): return # Didn't receive a full line. 163 | if line != "": 164 | state.handleMessage(line) 165 | else: 166 | state.sock.close() 167 | echo("Disconnected from hub: ", osErrorMsg()) 168 | echo("Reconnecting...") 169 | state.hubConnect() 170 | 171 | proc hubConnect(state: PState) = 172 | state.sock = asyncSocket() 173 | state.sock.connect("127.0.0.1", state.hubPort) 174 | state.sock.handleConnect = 175 | proc (s: PAsyncSocket) {.gcsafe.} = handleConnect(s, state) 176 | state.sock.handleRead = 177 | proc (s: PAsyncSocket) {.gcsafe.} = handleModuleMessage(s, state) 178 | state.dispatcher.register(state.sock) 179 | 180 | state.platform = "linux-x86" 181 | state.timeReconnected = -1.0 182 | 183 | proc open(port: TPort = TPort(9321), 184 | scgiPort: net.Port = net.Port(9323)): PState = 185 | new(result) 186 | 187 | result.dispatcher = newDispatcher() 188 | 189 | result.hubPort = port 190 | result.scgiPort = scgiPort 191 | result.subnets = @[] 192 | 193 | result.getCommandArgs() 194 | 195 | result.hubConnect() 196 | 197 | result.apiETag = "" 198 | getHookSubnets(result, timeout = -1) # Get initial set of subnets 199 | 200 | 201 | proc sendBuild(sock: PAsyncSocket, payload: PJsonNode) = 202 | var obj = newJObject() 203 | obj["payload"] = payload 204 | sock.send($obj & "\c\L") 205 | 206 | proc contains(subnets: seq[TSubnet], ip: string): bool = 207 | for subnet in subnets: 208 | if ip in subnet: 209 | return true 210 | 211 | proc isAuthorized(state: PState, ip: string): bool = 212 | result = ip in state.subnets 213 | if result == false: 214 | # Update subnet list 215 | getHookSubnets(state) 216 | result = ip in state.subnets 217 | 218 | when isMainModule: 219 | var state = open() 220 | 221 | settings: 222 | port = state.scgiPort 223 | 224 | routes: 225 | post "/": 226 | let realIP = 227 | if request.ip == "127.0.0.1": 228 | request.headers["X-Real-IP"] 229 | else: 230 | request.ip 231 | echo("[POST] ", realIP) 232 | var hostname = "" 233 | try: 234 | hostname = getHostByAddr(realIP).name 235 | except: 236 | hostname = getCurrentExceptionMsg() 237 | echo(" ", hostname) 238 | let authorized = state.isAuthorized(realIP) 239 | echo(" ", if authorized: "Authorized." else: "Denied.") 240 | cond authorized 241 | let payload = @"payload" 242 | 243 | echo(" Payload:") 244 | for line in splitLines(payload): 245 | echo(" ", line) 246 | 247 | var json = parseJSON(payload) 248 | if json.hasKey("after"): 249 | sendBuild(state.sock, json) 250 | echo(" ", json["after"].str) 251 | resp "Cheers, Github." 252 | 253 | while true: 254 | asyncdispatch.poll() 255 | discard state.dispatcher.poll() 256 | 257 | -------------------------------------------------------------------------------- /src/htmlhelp.nim: -------------------------------------------------------------------------------- 1 | import htmlgen, strtabs, strutils 2 | type 3 | THtmlTable* = object 4 | rows: seq[TRow] 5 | 6 | TRow* = seq[PColumn] 7 | PColumn* = ref TColumn 8 | TColumn* = tuple[header: bool, attrs: PStringTable, text: string] 9 | 10 | proc initTable*(): THtmlTable = 11 | result.rows = @[] 12 | 13 | proc addRow*(table: var THtmlTable, count = 1) = 14 | ## Adds `count` many rows to `table`. 15 | for i in 0..count-1: 16 | table.rows.add(@[]) 17 | 18 | proc addCol*(row: var TRow, text: string, isHeader = false, 19 | attrs: seq[tuple[name, content: string]] = @[]) = 20 | ## Adds column with the name of `text` to `row`. 21 | var c: PColumn 22 | new(c) 23 | c.header = isHeader 24 | c.attrs = newStringTable(modeCaseInsensitive) 25 | for key, val in items(attrs): 26 | c.attrs[key] = val 27 | c.text = text 28 | row.add(c) 29 | 30 | proc insertCol*(row: var TRow, i: int, text: string, isHeader = false, 31 | attrs: seq[tuple[name, content: string]] = @[]) = 32 | ## Inserts column with the name of `text` to `row` at index `i`. 33 | var c: PColumn 34 | new(c) 35 | c.header = isHeader 36 | c.attrs = newStringTable(modeCaseInsensitive) 37 | for key, val in items(attrs): 38 | c.attrs[key] = val 39 | c.text = text 40 | 41 | row.insert(c, i) 42 | 43 | proc `[]`*(table: var THtmlTable, i: int): var TRow = 44 | ## Retrieves row at `i` 45 | return table.rows[i] 46 | 47 | proc findCols*(row: var TRow, text: string): seq[PColumn] = 48 | ## Finds and returns columns with the name of `text`. 49 | result = @[] 50 | for c in row: 51 | if c.text == text: 52 | result.add(c) 53 | 54 | proc contains*(row: var TRow, text: string): bool = 55 | ## Returns whether `row` contains column by the name of `text`. 56 | result = false 57 | for c in row: 58 | if c.text == text: 59 | return true 60 | 61 | iterator items*(table: THtmlTable): TRow = 62 | var i = 0 63 | while i < table.rows.len: 64 | yield table.rows[i] 65 | i.inc() 66 | 67 | iterator items*(row: TRow): PColumn = 68 | var i = 0 69 | while i < row.len: 70 | yield row[i] 71 | i.inc() 72 | 73 | proc len*(table: var THtmlTable): int = 74 | ## Returns the number of rows. 75 | return table.rows.len() 76 | 77 | proc len*(row: TRow): int = 78 | ## Returns the number of columns in a row. 79 | return system.len(row) 80 | # Solely because using only `len` would cause a recursive loop. 81 | 82 | proc toPretty(table: THtmlTable): string = 83 | # Returns an ASCII representation of the table. 84 | # TODO: Make this nicer, or just get rid of it. 85 | result = "" 86 | for cols in table.rows: 87 | result.add("| ") 88 | for i in cols: 89 | result.add(i.text & " | ") 90 | result.add("\n----------------------------\n") 91 | 92 | proc toHtml*(table: THtmlTable, attrs=""): string = 93 | result = "" 94 | var htmlRows: string = "" 95 | for row in table.rows: 96 | var htmlCols = "" 97 | for col in row: 98 | var htmlAttrs = "" 99 | for name, text in col.attrs: 100 | htmlAttrs.add(" " & name & "=\"" & text & "\"") 101 | 102 | if col.header: 103 | htmlCols.add("\n$2\n" % [htmlAttrs, col.text]) 104 | else: 105 | htmlCols.add("\n$2\n" % [htmlAttrs, col.text]) 106 | htmlRows.add(tr(htmlCols) & "\n") 107 | 108 | result = "\n" % [attrs] & htmlRows & "
" 109 | 110 | when isMainModule: 111 | var tab = initTable() 112 | tab.addRow() 113 | tab.addRow() 114 | tab[0].addCol("Col 1", true, @[("class", "something"), ("blah", "something")]) 115 | tab[0].addCol("Col 2", true) 116 | tab[2].addCol("Hello") 117 | tab[2].addCol("I SHOULD BE IN SCHOOL") 118 | tab[2].addCol("With R.") 119 | tab[2].addCol("And be k....") 120 | echo tab.toPretty 121 | 122 | echo(" ") 123 | for r in tab: 124 | for c in r: 125 | echo(c.text) 126 | echo("----") 127 | 128 | echo tab.toHtml() 129 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | #! stdtmpl | standard 2 | #proc genHtml(state: PState): string = 3 | # result = "" 4 | 5 | 6 | 7 | NimBuild 8 | 9 | 10 | 11 | 12 |
13 | #var platforms: seq[string] = @[] # Every platform, from every commit. 14 | #var entries = getCommits(state.database, platforms) 15 | #platforms.sort(cmpPlatforms, Descending) 16 | 20 |
21 | 22 | ${genDownloadTable(state.req, entries, platforms)} 23 | #if platforms.len() > 0: 24 |
25 | ${genBuildResults(state, platforms, entries)} 26 |
27 | #else: 28 |

No commits found

29 | #end if 30 | 31 |

Platforms

32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | #for m in items(state.modules): 40 | #if m.name == "builder": 41 | #var platf = state.platforms[m.platform] 42 | 43 | 44 | 45 | 46 | 47 | 48 | #end if 49 | #end for 50 | 51 |
PlatformLagStatusDescription
${m.platform}${formatFloat(m.ping)}${platf}${platf.desc}
52 |
53 |
54 |
55 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/ircbot.nim: -------------------------------------------------------------------------------- 1 | import irc, sockets, asyncio, json, os, strutils, db, times, redis, irclog, marshal, streams, parseopt 2 | 3 | 4 | type 5 | PState = ref TState 6 | TState = object of TObject 7 | dispatcher: PDispatcher 8 | sock: PAsyncSocket 9 | ircClient: PAsyncIRC 10 | hubPort: TPort 11 | ircServerAddr: string 12 | database: TDb 13 | dbConnected: bool 14 | logger: PLogger 15 | irclogsFilename: string 16 | settings: TSettings 17 | birthdayWish: bool ## Did we wish a happy birthday? :) 18 | 19 | TSettings = object 20 | trustedUsers: seq[tuple[nick: string, host: string]] 21 | announceRepos: seq[string] 22 | announceChans: seq[string] 23 | announceNicks: seq[string] 24 | 25 | TSeenType = enum 26 | PSeenJoin, PSeenPart, PSeenMsg, PSeenNick, PSeenQuit 27 | 28 | TSeen = object 29 | nick: string 30 | channel: string 31 | timestamp: TTime 32 | case kind*: TSeenType 33 | of PSeenJoin: nil 34 | of PSeenPart, PSeenQuit, PSeenMsg: 35 | msg: string 36 | of PSeenNick: 37 | newNick: string 38 | 39 | const 40 | ircServer = "irc.freenode.net" 41 | joinChans = @["#nimrod"] 42 | botNickname = "NimBot" 43 | 44 | proc getCommandArgs(state: PState) = 45 | for kind, key, value in getOpt(): 46 | case kind 47 | of cmdArgument: 48 | quit("Syntax: ./ircbot [--hp hubPort] [--sa serverAddr] --il irclogsPath") 49 | of cmdLongOption, cmdShortOption: 50 | if value == "": 51 | quit("Syntax: ./ircbot [--hp hubPort] --il irclogsPath") 52 | case key 53 | of "serverAddr", "sa": 54 | state.ircServerAddr = value 55 | of "hubPort", "hp": 56 | state.hubPort = TPort(parseInt(value)) 57 | of "irclogs", "il": 58 | state.irclogsFilename = value 59 | else: quit("Syntax: ./ircbot [--hp hubPort] --il irclogsPath") 60 | of cmdEnd: assert false 61 | 62 | proc initSettings(settings: var TSettings) = 63 | settings.trustedUsers = @[(nick: "dom96", host: "unaffiliated/dom96")] 64 | settings.announceRepos = @["Araq/Nimrod"] 65 | settings.announceChans = @["#nimbuild"] 66 | settings.announceNicks = @["dom96"] 67 | 68 | proc saveSettings(state: PState) = 69 | store(newFileStream("nimbot.json", fmWrite), state.settings) 70 | 71 | proc setSeen(d: TDb, s: TSeen) = 72 | #if d.r.isNil: 73 | # echo("[Warning] Redis db nil") 74 | # return 75 | discard d.r.del("seen:" & s.nick) 76 | 77 | var hashToSet = @[("type", $s.kind.int), ("channel", s.channel), 78 | ("timestamp", $s.timestamp.int)] 79 | case s.kind 80 | of PSeenJoin: nil 81 | of PSeenPart, PSeenMsg, PSeenQuit: 82 | hashToSet.add(("msg", s.msg)) 83 | of PSeenNick: 84 | hashToSet.add(("newnick", s.newNick)) 85 | 86 | d.r.hMSet("seen:" & s.nick, hashToSet) 87 | 88 | proc getSeen(d: TDb, nick: string, s: var TSeen): bool = 89 | #if d.r.isNil: 90 | # echo("[Warning] Redis db nil") 91 | # return 92 | if d.r.exists("seen:" & nick): 93 | result = true 94 | s.nick = nick 95 | # Get the type first 96 | s.kind = d.r.hGet("seen:" & nick, "type").parseInt.TSeenType 97 | 98 | for key, value in d.r.hPairs("seen:" & nick): 99 | case normalize(key) 100 | of "type": 101 | # Type is retrieved before this. 102 | of "channel": 103 | s.channel = value 104 | of "timestamp": 105 | s.timestamp = TTime(value.parseInt) 106 | of "msg": 107 | s.msg = value 108 | of "newnick": 109 | s.newNick = value 110 | 111 | template createSeen(typ: TSeenType, n, c: string): stmt {.immediate, dirty.} = 112 | var seenNick: TSeen 113 | seenNick.kind = typ 114 | seenNick.nick = n 115 | seenNick.channel = c 116 | seenNick.timestamp = getTime() 117 | 118 | proc parseReply(line: string, expect: string): Bool = 119 | var jsonDoc = parseJson(line) 120 | return jsonDoc["reply"].str == expect 121 | 122 | proc limitCommitMsg(m: string): string = 123 | ## Limits the message to 300 chars and adds ellipsis. 124 | ## Also gets rid of \n, uses only the first line. 125 | var m1 = m 126 | if NewLines in m1: 127 | m1 = m1.splitLines()[0] 128 | 129 | if m1.len >= 300: 130 | m1 = m1[0..300] 131 | 132 | if m1.len >= 300 or NewLines in m: m1.add("... ") 133 | 134 | if NewLines in m: m1.add($(m.splitLines().len-1) & " more lines") 135 | 136 | return m1 137 | 138 | template pm(chan, msg: string): stmt = 139 | state.ircClient.privmsg(chan, msg) 140 | state.logger.log("NimBot", msg, chan) 141 | 142 | proc announce(state: PState, msg: string, important: bool) = 143 | var newMsg = "" 144 | if important: 145 | newMsg.add(join(state.settings.announceNicks, ",")) 146 | newMsg.add(": ") 147 | newMsg.add(msg) 148 | for i in state.settings.announceChans: 149 | pm(i, newMsg) 150 | 151 | proc isRepoAnnounced(state: PState, url: string): bool = 152 | result = false 153 | for repo in state.settings.announceRepos: 154 | if url.ToLower().endswith(repo.ToLower()): 155 | return true 156 | 157 | proc getBranch(theRef: string): string = 158 | if theRef.startswith("refs/heads/"): 159 | result = theRef[11 .. -1] 160 | else: 161 | result = theRef 162 | 163 | proc handleWebMessage(state: PState, line: string) = 164 | echo("Got message from hub: " & line) 165 | var json = parseJson(line) 166 | if json.existsKey("payload"): 167 | if isRepoAnnounced(state, json["payload"]["repository"]["url"].str): 168 | let commitsToAnnounce = min(4, json["payload"]["commits"].len) 169 | if commitsToAnnounce != 0: 170 | for i in 0..commitsToAnnounce-1: 171 | var commit = json["payload"]["commits"][i] 172 | # Create the message 173 | var message = "" 174 | message.add(json["payload"]["repository"]["owner"]["name"].str & "/" & 175 | json["payload"]["repository"]["name"].str & " ") 176 | message.add(json["payload"]["ref"].str.getBranch() & " ") 177 | message.add(commit["id"].str[0..6] & " ") 178 | message.add(commit["author"]["name"].str & " ") 179 | message.add("[+" & $commit["added"].len & " ") 180 | message.add("±" & $commit["modified"].len & " ") 181 | message.add("-" & $commit["removed"].len & "]: ") 182 | message.add(limitCommitMsg(commit["message"].str)) 183 | 184 | # Send message to #nimrod. 185 | pm(joinChans[0], message) 186 | if commitsToAnnounce != json["payload"]["commits"].len: 187 | let unannounced = json["payload"]["commits"].len-commitsToAnnounce 188 | pm(joinChans[0], $unannounced & " more commits.") 189 | else: 190 | # New branch 191 | var message = "" 192 | message.add(json["payload"]["repository"]["owner"]["name"].str & "/" & 193 | json["payload"]["repository"]["name"].str & " ") 194 | let theRef = json["payload"]["ref"].str.getBranch() 195 | if existsKey(json["payload"], "base_ref"): 196 | let baseRef = json["payload"]["base_ref"].str.getBranch() 197 | message.add("New branch: " & baseRef & " -> " & theRef) 198 | else: 199 | message.add("New branch: " & theRef) 200 | 201 | message.add(" by " & json["payload"]["pusher"]["name"].str) 202 | 203 | elif json.existsKey("redisinfo"): 204 | assert json["redisinfo"].existsKey("port") 205 | let redisPort = json["redisinfo"]["port"].num 206 | state.database = db.open(port = TPort(redisPort)) 207 | state.dbConnected = true 208 | elif json.existsKey("announce"): 209 | announce(state, json["announce"].str, json["important"].bval) 210 | 211 | proc hubConnect(state: PState) 212 | proc handleConnect(s: PAsyncSocket, state: PState) = 213 | try: 214 | # Send greeting 215 | var obj = newJObject() 216 | obj["name"] = newJString("irc") 217 | obj["platform"] = newJString("?") 218 | obj["version"] = %"1" 219 | state.sock.send($obj & "\c\L") 220 | 221 | # Wait for reply. 222 | var line = "" 223 | sleep(1500) 224 | if state.sock.recvLine(line): 225 | assert(line != "") 226 | doAssert parseReply(line, "OK") 227 | echo("The hub accepted me!") 228 | else: 229 | raise newException(EInvalidValue, 230 | "Hub didn't accept me. Waited 1.5 seconds.") 231 | 232 | # ask for the redis info 233 | var riobj = newJObject() 234 | riobj["do"] = newJString("redisinfo") 235 | state.sock.send($riobj & "\c\L") 236 | 237 | except EOS, EInvalidValue, EAssertionFailed: 238 | echo(getCurrentExceptionMsg()) 239 | s.close() 240 | echo("Waiting 5 seconds...") 241 | sleep(5000) 242 | state.hubConnect() 243 | 244 | proc handleRead(s: PAsyncSocket, state: PState) = 245 | var line = "" 246 | if state.sock.recvLine(line): 247 | if line != "": 248 | # Handle the message 249 | state.handleWebMessage(line) 250 | else: 251 | echo("Disconnected from hub: ", OSErrorMsg()) 252 | announce(state, "Got disconnected from hub! " & OSErrorMsg(), true) 253 | state.sock.close() 254 | echo("Reconnecting...") 255 | state.hubConnect() 256 | else: 257 | echo(OSErrorMsg()) 258 | 259 | proc hubConnect(state: PState) = 260 | state.sock = AsyncSocket() 261 | state.sock.connect("127.0.0.1", state.hubPort) 262 | state.sock.handleConnect = proc (s: PAsyncSocket) = handleConnect(s, state) 263 | state.sock.handleRead = proc (s: PAsyncSocket) = handleRead(s, state) 264 | 265 | state.dispatcher.register(state.sock) 266 | 267 | proc isUserTrusted(state: PState, nick, host: string): bool = 268 | for i in state.settings.trustedUsers: 269 | if i.nick == nick and i.host == host: 270 | return true 271 | return false 272 | 273 | proc addDup[T](s: var seq[T], v: T) = 274 | ## Adds only if it doesn't already exist in seq. 275 | if v notin s: 276 | s.add(v) 277 | 278 | proc delTrust(s: var seq[tuple[nick: string, host: string]], nick, host: string): bool = 279 | for i in 0..s.len-1: 280 | if s[i].nick == nick and s[i].host == host: 281 | s.del(i) 282 | return true 283 | return false 284 | 285 | proc del[T](s: var seq[T], v: T): bool = 286 | for i in 0..s.len-1: 287 | if s[i] == v: 288 | s.del(i) 289 | return true 290 | return false 291 | 292 | proc `$`(s: seq[tuple[nick: string, host: string]]): string = 293 | result = "" 294 | for i in s: 295 | result.add(i.nick & "@" & i.host & ", ") 296 | result = result[0 .. -3] 297 | 298 | proc isFilwitBirthday(): bool = 299 | result = false 300 | let t = getTime().getGMTime() 301 | if t.month == mSep: 302 | if t.monthday == 10 and t.hour >= 19: 303 | return true 304 | if t.monthday == 11 and t.hour <= 8: 305 | return true 306 | 307 | proc handleIrc(irc: PAsyncIRC, event: TIRCEvent, state: PState) = 308 | case event.typ 309 | of EvConnected: nil 310 | of EvDisconnected: 311 | echo("Disconnected from server.") 312 | state.ircClient.reconnect() 313 | of EvMsg: 314 | echo("< ", event.raw) 315 | # Logs: 316 | state.logger.log(event) 317 | template pmOrig(msg: string) = 318 | pm(event.origin, msg) 319 | case event.cmd 320 | of MPrivMsg: 321 | let msg = event.params[event.params.len-1] 322 | let words = msg.split(' ') 323 | case words[0] 324 | of "!ping": pmOrig("pong") 325 | of "!lag": 326 | if state.ircClient.getLag != -1.0: 327 | var lag = state.ircClient.getLag 328 | lag = lag * 1000.0 329 | pmOrig($int(lag) & "ms between me and the server.") 330 | else: 331 | pmOrig("Unknown.") 332 | of "!seen": 333 | if words.len > 1: 334 | let nick = words[1] 335 | if nick == botNickname: 336 | pmOrig("Yes, I see myself.") 337 | var seenInfo: TSeen 338 | if state.database.getSeen(nick, seenInfo): 339 | case seenInfo.kind 340 | of PSeenMsg: 341 | pmOrig("$1 was last seen on $2 in $3 saying: $4" % 342 | [seenInfo.nick, $seenInfo.timestamp, 343 | seenInfo.channel, seenInfo.msg]) 344 | of PSeenJoin: 345 | pmOrig("$1 was last seen on $2 joining $3" % 346 | [seenInfo.nick, $seenInfo.timestamp, seenInfo.channel]) 347 | of PSeenPart: 348 | pmOrig("$1 was last seen on $2 leaving $3 with message: $4" % 349 | [seenInfo.nick, $seenInfo.timestamp, seenInfo.channel, 350 | seenInfo.msg]) 351 | of PSeenQuit: 352 | pmOrig("$1 was last seen on $2 quitting with message: $3" % 353 | [seenInfo.nick, $seenInfo.timestamp, seenInfo.msg]) 354 | of PSeenNick: 355 | pmOrig("$1 was last seen on $2 changing nick to $3" % 356 | [seenInfo.nick, $seenInfo.timestamp, seenInfo.newNick]) 357 | 358 | else: 359 | pmOrig("I have not seen " & nick) 360 | else: 361 | pmOrig("Syntax: !seen ") 362 | of "!addtrust": 363 | if words.len > 2: 364 | if isUserTrusted(state, event.nick, event.host): 365 | state.settings.trustedUsers.addDup((words[1], words[2])) 366 | saveSettings(state) 367 | pmOrig("Done.") 368 | else: 369 | pmOrig("Access denied.") 370 | else: 371 | pmOrig("Syntax: !addtrust ") 372 | of "!remtrust": 373 | if words.len > 2: 374 | if isUserTrusted(state, event.nick, event.host): 375 | if state.settings.trustedUsers.delTrust(words[1], words[2]): 376 | saveSettings(state) 377 | pmOrig("Done.") 378 | else: 379 | pmOrig("Could not find user") 380 | else: 381 | pmOrig("Access denied.") 382 | else: 383 | pmOrig("Syntax: !remtrust ") 384 | of "!trusted": 385 | pmOrig("Trusted users: " & $state.settings.trustedUsers) 386 | of "!addrepo": 387 | if words.len > 2: 388 | if isUserTrusted(state, event.nick, event.host): 389 | state.settings.announceRepos.addDup(words[1] & "/" & words[2]) 390 | saveSettings(state) 391 | pmOrig("Done.") 392 | else: 393 | pmOrig("Access denied.") 394 | else: 395 | pmOrig("Syntax: !addrepo ") 396 | of "!remrepo": 397 | if words.len > 2: 398 | if isUserTrusted(state, event.nick, event.host): 399 | if state.settings.announceRepos.del(words[1] & "/" & words[2]): 400 | saveSettings(state) 401 | pmOrig("Done.") 402 | else: 403 | pmOrig("Repo not found.") 404 | else: 405 | pmOrig("Access denied.") 406 | else: 407 | pmOrig("Syntax: !remrepo ") 408 | of "!repos": 409 | pmOrig("Announced repos: " & state.settings.announceRepos.join(", ")) 410 | of "!addnick": 411 | if words.len > 1: 412 | if isUserTrusted(state, event.nick, event.host): 413 | state.settings.announceNicks.addDup(words[1]) 414 | saveSettings(state) 415 | pmOrig("Done.") 416 | else: 417 | pmOrig("Access denied.") 418 | else: 419 | pmOrig("Syntax: !addnick ") 420 | of "!remnick": 421 | if words.len > 1: 422 | if isUserTrusted(state, event.nick, event.host): 423 | if state.settings.announceNicks.del(words[1]): 424 | saveSettings(state) 425 | pmOrig("Done.") 426 | else: 427 | pmOrig("Nick not found.") 428 | else: 429 | pmOrig("Access denied.") 430 | else: 431 | pmOrig("Syntax: !remnick ") 432 | of "!nicks": 433 | pmOrig("Announce nicks: " & state.settings.announceNicks.join(", ")) 434 | 435 | if words[0].startswith("!kirbyrape"): 436 | pmOrig("(>^(>O_O)>") 437 | 438 | # TODO: ... commands 439 | 440 | # -- Seen 441 | # Log this as activity. 442 | createSeen(PSeenMsg, event.nick, event.origin) 443 | seenNick.msg = msg 444 | state.database.setSeen(seenNick) 445 | of MJoin: 446 | createSeen(PSeenJoin, event.nick, event.origin) 447 | state.database.setSeen(seenNick) 448 | if event.nick == "filwit" and isFilwitBirthday() and (not state.birthdayWish): 449 | pmOrig("Happy birthday to you, happy birthday to you! Happy BIRTHDAY " & 450 | "dear filwit! happy birthday to you!!!") 451 | state.birthdayWish = true 452 | of MPart: 453 | createSeen(PSeenPart, event.nick, event.origin) 454 | let msg = event.params[event.params.high] 455 | seenNick.msg = msg 456 | state.database.setSeen(seenNick) 457 | of MQuit: 458 | createSeen(PSeenQuit, event.nick, event.origin) 459 | let msg = event.params[event.params.high] 460 | seenNick.msg = msg 461 | state.database.setSeen(seenNick) 462 | of MNick: 463 | createSeen(PSeenNick, event.nick, "#nimrod") 464 | seenNick.newNick = event.params[0] 465 | state.database.setSeen(seenNick) 466 | of MNumeric: 467 | if event.numeric == "433": 468 | # Nickname already in use. 469 | irc.send("NICK " & irc.getNick() & "_") 470 | else: 471 | nil # TODO: ? 472 | 473 | proc open(port: TPort = TPort(5123)): PState = 474 | var cres: PState 475 | new(cres) 476 | cres.dispatcher = newDispatcher() 477 | cres.settings.initSettings() 478 | if existsFile("nimbot.json"): 479 | load(newFileStream("nimbot.json", fmRead), cres.settings) 480 | 481 | cres.hubPort = port 482 | cres.irclogsFilename = "" 483 | cres.ircServerAddr = ircServer 484 | cres.getCommandArgs() 485 | 486 | if cres.irclogsFilename == "": 487 | quit("You need to specify the irclogs filename.") 488 | 489 | cres.hubConnect() 490 | 491 | # Connect to the irc server. 492 | let ie = proc (irc: PAsyncIRC, event: TIRCEvent) = 493 | handleIrc(irc, event, cres) 494 | var joinChannels = joinChans 495 | joinChannels.add(cres.settings.announceChans) 496 | cres.ircClient = AsyncIrc(cres.ircServerAddr, nick = botNickname, 497 | user = botNickname, joinChans = joinChannels, ircEvent = ie) 498 | cres.ircClient.connect() 499 | cres.dispatcher.register(cres.ircClient) 500 | 501 | cres.dbConnected = false 502 | 503 | cres.logger = newLogger(cres.irclogsFilename) 504 | result = cres 505 | 506 | proc isBDFLsBirthday(): bool = 507 | result = false 508 | let t = getTime().getGMTime() 509 | if t.month == mJun: 510 | if t.monthday == 16 and t.hour >= 22: 511 | return true 512 | if t.monthday == 17 and t.hour <= 21: 513 | return true 514 | 515 | var state = ircbot.open() # Connect to the website and the IRC server. 516 | 517 | while state.dispatcher.poll(): 518 | if state.dbConnected: 519 | state.database.keepAlive() 520 | 521 | if isBDFLsBirthday() and not state.birthdayWish: 522 | pm("#nimrod", "It's Araq's birthday today! Everybody wish our great BDFL a happy birthday!!!") 523 | state.birthdayWish = true 524 | -------------------------------------------------------------------------------- /src/irclog.nim: -------------------------------------------------------------------------------- 1 | import htmlgen, times, irc, streams, strutils, os, json, parseutils, marshal 2 | from xmltree import escape 3 | 4 | type 5 | TLogger* = object of TObject # Items get erased when new day starts. 6 | startTime*: TTimeInfo 7 | logFilepath*: string 8 | logFile*: TFile 9 | PLogger* = ref TLogger 10 | 11 | const 12 | webFP = {fpUserRead, fpUserWrite, fpUserExec, 13 | fpGroupRead, fpGroupExec, fpOthersRead, fpOthersExec} 14 | 15 | proc loadLogger*(f: string): PLogger = 16 | new(result) 17 | let logs = readFile(f) 18 | let lines = logs.splitLines() 19 | # Line 1: Start time 20 | result.startTime = fromSeconds(to[float](lines[0])).getGMTime() 21 | 22 | doAssert open(result.logFile, f, fmAppend) 23 | result.logFilepath = f.splitFile.dir 24 | 25 | proc writeFlush(file: TFile, s: string) = 26 | file.write(s) 27 | file.flushFile() 28 | 29 | proc newLogger*(logFilepath: string): PLogger = 30 | let startTime = getTime().getGMTime() 31 | let log = logFilepath / startTime.format("dd'-'MM'-'yyyy'.logs'") 32 | if existsFile(log): 33 | result = loadLogger(log) 34 | else: 35 | new(result) 36 | result.startTime = startTime 37 | result.logFilepath = logFilepath 38 | doAssert open(result.logFile, log, fmAppend) 39 | # Write start time 40 | result.logFile.writeFlush($$epochTime() & "\n") 41 | 42 | proc `$`(s: seq[string]): string = 43 | var escaped = system.map(s) do (x: string) -> string: 44 | strutils.escape(x) 45 | result = "[" & join(escaped, ",") & "]" 46 | 47 | proc writeLog(logger: PLogger, msg: TIRCEvent) = 48 | logger.logFile.writeFlush($$(time: getTime(), msg: msg) & "\n") 49 | 50 | proc log*(logger: PLogger, msg: TIRCEvent) = 51 | if msg.origin != "#nimrod" and msg.cmd notin {MQuit, MNick}: return 52 | if getTime().getGMTime().yearday != logger.startTime.yearday: 53 | # It's time to cycle to next day. 54 | # Reset logger. 55 | logger.logFile.close() 56 | logger.startTime = getTime().getGMTime() 57 | let log = logger.logFilepath / logger.startTime.format("dd'-'MM'-'yyyy'.logs'") 58 | doAssert open(logger.logFile, log, fmAppend) 59 | # Write start time 60 | logger.logFile.writeFlush($epochTime() & "\n") 61 | 62 | case msg.cmd 63 | of MPrivMsg, MJoin, MPart, MNick, MQuit: # TODO: MTopic? MKick? 64 | #logger.items.add((getTime(), msg)) 65 | #logger.save(logger.logFilepath / logger.startTime.format("dd'-'MM'-'yyyy'.json'")) 66 | writeLog(logger, msg) 67 | else: nil 68 | 69 | proc log*(logger: PLogger, nick, msg, chan: string) = 70 | var m: TIRCEvent 71 | m.typ = EvMsg 72 | m.cmd = MPrivMsg 73 | m.params = @[chan, msg] 74 | m.origin = chan 75 | m.nick = nick 76 | logger.log(m) 77 | 78 | when isMainModule: 79 | var logger = newLogger("testing/logstest") 80 | logger.log("dom96", "Hello!", "#nimrod") 81 | logger.log("dom96", "Hello\r, testingí, \"\"", "#nimrod") 82 | #logger = loadLogger("testing/logstest/26-05-2013.logs") 83 | echo repr(logger) -------------------------------------------------------------------------------- /src/irclogrender.nim: -------------------------------------------------------------------------------- 1 | import irc, htmlgen, times, strutils, marshal, os, xmltree 2 | from jester import TRequest, makeUri 3 | import irclog 4 | 5 | type 6 | TLogRenderer = object of TLogger 7 | items*: seq[tuple[time: TTime, msg: TIRCEvent]] ## Only used for HTML gen 8 | PLogRenderer* = ref TLogRenderer 9 | 10 | proc loadRenderer*(f: string): PLogRenderer = 11 | new(result) 12 | result.items = @[] 13 | let logs = readFile(f) 14 | let lines = logs.splitLines() 15 | var i = 1 16 | # Line 1: Start time 17 | result.startTime = fromSeconds(to[float](lines[0])).getGMTime() 18 | 19 | result.logFilepath = f.splitFile.dir 20 | while i < lines.len: 21 | if lines[i] != "": 22 | result.items.add(to[tuple[time: TTime, msg: TIRCEvent]](lines[i])) 23 | inc i 24 | 25 | proc renderItems(logger: PLogRenderer): string = 26 | result = "" 27 | for i in logger.items: 28 | var c = "" 29 | case i.msg.cmd 30 | of MJoin: 31 | c = "join" 32 | of MPart: 33 | c = "part" 34 | of MNick: 35 | c = "nick" 36 | of MQuit: 37 | c = "quit" 38 | else: 39 | nil 40 | var message = i.msg.params[i.msg.params.len-1] 41 | if message.startswith("\x01ACTION "): 42 | c = "action" 43 | message = message[8 .. -2] 44 | 45 | if c == "": 46 | result.add(tr(td(i.time.getGMTime().format("HH':'mm':'ss")), 47 | td(class="nick", xmltree.escape(i.msg.nick)), 48 | td(class="msg", xmltree.escape(message)))) 49 | else: 50 | case c 51 | of "join": 52 | message = i.msg.nick & " joined " & i.msg.origin 53 | of "part": 54 | message = i.msg.nick & " left " & i.msg.origin & " (" & message & ")" 55 | of "nick": 56 | message = i.msg.nick & " is now known as " & message 57 | of "quit": 58 | message = i.msg.nick & " quit (" & message & ")" 59 | of "action": 60 | message = i.msg.nick & " " & message 61 | else: assert(false) 62 | result.add(tr(class=c, 63 | td(i.time.getGMTime().format("HH':'mm':'ss")), 64 | td(class="nick", "*"), 65 | td(class="msg", xmltree.escape(message)))) 66 | 67 | proc renderHtml*(logger: PLogRenderer, req: jester.TRequest): string = 68 | let today = getTime().getGMTime() 69 | let isToday = logger.startTime.monthday == today.monthday and 70 | logger.startTime.month == today.month and 71 | logger.startTime.year == today.year 72 | let previousDay = logger.startTime - (initInterval(days=1)) 73 | let prevUrl = req.makeUri("irclogs/" & 74 | previousDay.format("dd'-'MM'-'yyyy'.html'"), 75 | absolute = false) 76 | let nextDay = logger.startTime + (initInterval(days=1)) 77 | let nextUrl = 78 | if isToday: "" 79 | else: req.makeUri("irclogs/" & 80 | nextDay.format("dd'-'MM'-'yyyy'.html'"), absolute = false) 81 | result = 82 | html( 83 | head(title("#nimrod logs for " & logger.startTime.format("dd'-'MM'-'yyyy")), 84 | meta(content="text/html; charset=UTF-8", `http-equiv` = "Content-Type"), 85 | link(rel="stylesheet", href=req.makeUri("css/boilerplate.css", absolute = false)), 86 | link(rel="stylesheet", href=req.makeUri("css/log.css", absolute = false)) 87 | ), 88 | body( 89 | htmlgen.`div`(id="controls", 90 | a(href=prevUrl, "<<"), 91 | span(logger.startTime.format("dd'-'MM'-'yyyy")), 92 | (if nextUrl == "": span(">>") else: a(href=nextUrl, ">>")) 93 | ), 94 | hr(), 95 | table( 96 | renderItems(logger) 97 | ) 98 | ) 99 | ) -------------------------------------------------------------------------------- /src/types.nim: -------------------------------------------------------------------------------- 1 | import os, tables, hashes 2 | # TODO: Rename this module to ``utils`` 3 | type 4 | TBuilderJob* = enum 5 | jBuild, jTest, jDocGen, jCSrcGen, jInnoSetup 6 | 7 | TProgress* = enum 8 | jUnknown, jFail, jInProgress, jSuccess 9 | 10 | TStatus* = object 11 | isInProgress*: bool 12 | desc*: string 13 | hash*: string 14 | branch*: string 15 | jobs*: TTable[TBuilderJob, TProgress] 16 | cmd*: string 17 | args*: string 18 | FTPSpeed*: float 19 | 20 | TBuilderEventType* = enum 21 | bProcessStart, bProcessLine, bProcessExit, bFTPUploadSpeed, bEnd, bStart 22 | 23 | proc hash*[T: enum](x: T): THash = ord(x) 24 | 25 | proc initStatus*(): TStatus = 26 | result.isInProgress = false 27 | result.jobs = initTable[TBuilderJob, TProgress]() 28 | result.desc = "" 29 | result.hash = "" 30 | result.cmd = "" 31 | result.args = "" 32 | result.FTPSpeed = -1.0 33 | 34 | proc jobInProgress*(s: TStatus): TBuilderJob = 35 | assert s.isInProgress 36 | for j, p in s.jobs: 37 | if p == jInProgress: 38 | return j 39 | raise newException(EInvalidValue, "No job could be found that is in progress.") 40 | 41 | proc findLatestJob*(s: TStatus, job: var TBuilderJob): bool = 42 | for i in TBuilderJob: 43 | if s.jobs[i] == jFail or s.jobs[i] == jSuccess: 44 | job = i 45 | return true 46 | 47 | proc `$`*(s: TStatus): string = 48 | if s.isInProgress: 49 | let job = jobInProgress(s) 50 | case job 51 | of jBuild: 52 | result = "Bootstrapping" 53 | of jTest: 54 | result = "Testing" 55 | of jDocGen: 56 | result = "Generating docs" 57 | of jCSrcGen: 58 | result = "Generating C Sources" 59 | of jInnoSetup: 60 | result = "Generating Inno setup file" 61 | else: 62 | var job: TBuilderJob 63 | result = "Unknown" 64 | if findLatestJob(s, job): 65 | case job 66 | of jBuild: 67 | if s.jobs[job] == jSuccess: 68 | result = "Bootstrapped successfully" 69 | elif s.jobs[job] == jFail: 70 | result = "Bootstrapping failed" 71 | of jTest: 72 | if s.jobs[job] == jSuccess: 73 | result = "Tested successfully" 74 | elif s.jobs[job] == jFail: 75 | result = "Testing failed" 76 | of jDocGen: 77 | if s.jobs[job] == jSuccess: 78 | result = "Doc generation succeeded" 79 | elif s.jobs[job] == jFail: 80 | result = "Doc generation failed" 81 | of jCSrcGen: 82 | if s.jobs[job] == jSuccess: 83 | result = "C source generation succeeded" 84 | elif s.jobs[job] == jFail: 85 | result = "C source generation failed" 86 | of jInnoSetup: 87 | if s.jobs[job] == jSuccess: 88 | result = "Inno setup generation succeeded" 89 | elif s.jobs[job] == jFail: 90 | result = "Inno setup generation failed" 91 | 92 | proc makeCommitPath*(platform, hash: string): string = 93 | return platform / hash.substr(0, 11) # 11 Chars. 94 | 95 | proc makeZipPath*(platform, hash: string): string = 96 | return platform / "nimrod_" & hash.substr(0, 11) 97 | 98 | proc makeInnoSetupPath*(hash: string): string = 99 | return ("nimrod_" & hash.substr(0, 11)) & ".exe" 100 | -------------------------------------------------------------------------------- /src/website.nim: -------------------------------------------------------------------------------- 1 | ## This is the SCGI Website and the hub. 2 | import 3 | sockets, asyncio, json, strutils, os, scgi, strtabs, times, streams, parsecfg, 4 | algorithm, tables, base64 5 | import htmlgen except del 6 | import types, db, htmlhelp 7 | from httpclient import nil 8 | from net import nil 9 | 10 | import asyncdispatch except newDispatcher, Port 11 | 12 | import jester 13 | 14 | type 15 | TBQCommit = object 16 | hash: string 17 | branch: string 18 | payload: PJsonNode 19 | 20 | PState = ref TState 21 | TState = object of TObject 22 | dispatcher: PDispatcher 23 | sock: PAsyncSocket ## Hub server socket. All modules connect to this. 24 | req: Request 25 | jesterSettings: jester.Settings 26 | modules: seq[TModule] 27 | database: TDb 28 | platforms: TTable[string, TStatus] 29 | buildQueue: TTable[string, seq[TBQCommit]] # Platform, [(hash, branch, payload)] 30 | password: string ## The password that foreign modules need to be accepted. 31 | bindAddr: string 32 | bindPort: int 33 | scgiPort: int 34 | redisPort: int 35 | isHttp: bool 36 | ircLogsPath: string 37 | packagesJson: string # List of babel packages from nimrod-code/packages 38 | 39 | TModuleStatus = enum 40 | MSConnecting, ## Module connected, but has not sent the greeting. 41 | MSConnected ## Module is ready to do work. 42 | 43 | TModule = object 44 | name: string 45 | sock: PAsyncSocket ## Client socket 46 | status: TModuleStatus 47 | platform: string 48 | lastPong: float 49 | pinged: bool # whether we are waiting for a pong from the module. 50 | ping: float # in seconds 51 | ip: string # IP address this module is connecting from. 52 | delegID: PDelegate 53 | logFile: TFile # Only applicable to a module of type builder. 54 | 55 | proc parseConfig(state: PState, path: string) = 56 | var f = newFileStream(path, fmRead) 57 | if f != nil: 58 | var p: TCfgParser 59 | open(p, f, path) 60 | var count = 0 61 | while true: 62 | var n = next(p) 63 | case n.kind 64 | of cfgEof: 65 | break 66 | of cfgSectionStart: 67 | raise newException(EInvalidValue, "Unknown section: " & n.section) 68 | of cfgKeyValuePair, cfgOption: 69 | case normalize(n.key) 70 | of "bindaddr": 71 | state.bindAddr = n.value 72 | inc(count) 73 | of "bindport": 74 | state.bindPort = parseInt(n.value) 75 | inc(count) 76 | of "scgiport": 77 | state.scgiPort = parseInt(n.value) 78 | inc(count) 79 | of "redisport": 80 | state.redisPort = parseInt(n.value) 81 | inc(count) 82 | of "password": 83 | state.password = n.value 84 | inc(count) 85 | of "ishttp": 86 | state.isHttp = n.value.normalize == "true" 87 | inc(count) 88 | of "irclogspath": 89 | state.ircLogsPath = n.value 90 | inc(count) 91 | of cfgError: 92 | raise newException(EInvalidValue, "Configuration parse error: " & n.msg) 93 | if count < 7: 94 | quit("Not all settings have been specified in the .ini file") 95 | close(p) 96 | else: 97 | quit("Cannot open configuration file: " & path) 98 | 99 | proc handleAccept(s: PAsyncSocket, state: PState) {.gcsafe.} 100 | proc open(configPath: string): PState = 101 | var cres: PState 102 | new(cres) 103 | parseConfig(cres, configPath) 104 | cres.dispatcher = newDispatcher() 105 | 106 | cres.sock = asyncSocket() 107 | cres.sock.bindAddr(TPort(cres.bindPort), cres.bindAddr) 108 | cres.sock.listen() 109 | cres.sock.handleAccept = proc (s: PAsyncSocket) = handleAccept(s, cres) 110 | cres.modules = @[] 111 | cres.platforms = initTable[string, TStatus]() 112 | cres.buildQueue = initTable[string, seq[TBQCommit]]() 113 | 114 | cres.dispatcher.register(cres.sock) 115 | 116 | # Connect to the database 117 | try: 118 | cres.database = db.open("localhost", TPort(cres.redisPort)) 119 | except EOS: 120 | quit("Couldn't connect to redis: " & getCurrentExceptionMsg()) 121 | 122 | result = cres 123 | 124 | # Modules 125 | 126 | proc contains(modules: seq[TModule], name: string): bool = 127 | for i in items(modules): 128 | if i.name == name: return true 129 | 130 | return false 131 | 132 | proc handleModuleMsg(s: PAsyncSocket, arg: PObject) {.gcsafe.} 133 | proc addModule(state: PState, client: PAsyncSocket, IPAddr: string) = 134 | var module: TModule 135 | module.sock = client 136 | module.ip = IPAddr 137 | module.lastPong = epochTime() 138 | module.pinged = false 139 | module.status = MSConnecting 140 | echo(IPAddr, " connected.") 141 | 142 | # Add this module to the dispatcher. 143 | client.handleRead = proc (s:PAsyncSocket) = handleModuleMsg(s, state) 144 | module.delegID = state.dispatcher.register(client) 145 | 146 | state.modules.add(module) 147 | 148 | proc findBuilderModule(state: PState, platf: string, module: var TModule): bool = 149 | result = false 150 | for i in state.modules: 151 | if i.name == "builder" and i.platform == platf: 152 | module = i 153 | return true 154 | 155 | proc mGetBuilderModule(state: PState, platf: string): var TModule = 156 | for i in 0..state.modules.len-1: 157 | if state.modules[i].name == "builder" and state.modules[i].platform == platf: 158 | return state.modules[i] 159 | raise newException(EInvalidValue, "Platform could not be found.") 160 | 161 | proc parseGreeting(state: PState, m: var TModule, line: string, errMsg: var string): bool = 162 | # { "name": "modulename", "version": "1" } 163 | # optional params: settings 164 | var json: PJsonNode 165 | try: 166 | json = parseJson(line) 167 | except EJsonParsingError: 168 | return false 169 | 170 | if m.ip != "127.0.0.1": 171 | # Check for password 172 | var fail = true 173 | if json.existsKey("pass"): 174 | if json["pass"].str == state.password: 175 | fail = false 176 | else: 177 | echo("Got incorrect password: ", json["pass"].str) 178 | errMsg = "Invalid password" 179 | 180 | if fail: return false 181 | 182 | if not (json.existsKey("name") and json.existsKey("platform")): 183 | errMsg = "Invalid greeting." 184 | return false 185 | if not json.existsKey("version"): 186 | errMsg = "Required version field missing." 187 | return false 188 | else: 189 | if json["version"].str != "1": 190 | errMsg = "Invalid version." 191 | return false 192 | 193 | m.name = json["name"].str 194 | m.platform = json["platform"].str 195 | 196 | # Only add this module platform to platforms if it's a `builder`, and 197 | # if platform doesn't already exist. 198 | if m.name == "builder": 199 | if not state.platforms.hasKey(m.platform): 200 | state.platforms[m.platform] = initStatus() 201 | else: 202 | echo("Platform(", m.platform, ") already exists.") 203 | errMsg = "This platform already exists." 204 | return false 205 | 206 | m.status = MSConnected 207 | 208 | return true 209 | 210 | proc uniqueMName(module: TModule): string = 211 | result = "" 212 | case module.status 213 | of MSConnected: 214 | result.add module.name 215 | result.add "-" 216 | result.add module.platform 217 | result.add "(" & module.ip & ")" 218 | of MSConnecting: 219 | result.add "Unknown (" & module.ip & ")" 220 | 221 | proc IRCAnnounce(state: PState, msg: string, important = false) = 222 | if "irc" in state.modules: 223 | for module in items(state.modules): 224 | if module.name == "irc": 225 | let json = %{"announce": %msg, "important": %important} 226 | module.sock.send($json & "\c\L") 227 | 228 | proc remove(state: PState, module: TModule) = 229 | for i in 0..len(state.modules)-1: 230 | var m = state.modules[i] 231 | if m.name == module.name and 232 | m.platform == module.platform and 233 | m.ip == module.ip: 234 | state.dispatcher.unregister(state.modules[i].delegID) 235 | state.modules[i].sock.close() 236 | echo(uniqueMName(state.modules[i]), " disconnected.") 237 | if m.name != "irc": 238 | IRCAnnounce(state, uniqueMName(state.modules[i]) & " disconnected.", true) 239 | # if module is a builder remove it from platforms. 240 | if m.name == "builder": 241 | state.platforms.del(m.platform) 242 | 243 | state.modules.delete(i) 244 | return 245 | 246 | proc setJob(state: PState, p: string, job: TBuilderJob) = 247 | var s = state.platforms[p] 248 | s.isInProgress = true 249 | s.desc = "" 250 | s.jobs[job] = jInProgress 251 | state.platforms[p] = s 252 | 253 | proc setResult(state: PState, p: string, res: TResult, detail: string) = 254 | var s = state.platforms[p] 255 | let job = jobInProgress(s) 256 | s.isInProgress = false 257 | s.jobs[job] = if res == Success: jSuccess else: jFail 258 | s.desc = detail 259 | state.platforms[p] = s 260 | 261 | proc setDesc(state: PState, p: string, desc: string) = 262 | var s = state.platforms[p] 263 | assert s.isInProgress 264 | s.desc = desc 265 | state.platforms[p] = s 266 | 267 | proc writeBuildSpecificLogs(state: PState, platf: string, line: string) = 268 | let m = mGetBuilderModule(state, platf) 269 | if m.logFile == nil: 270 | # TODO: This will happen if a builder reconnects during a build, 271 | # the builder should send some sort of command to reopen the logFile upon 272 | # reconnection 273 | echo("Warning: Could not write to logfile as it is nil.") 274 | return 275 | m.logFile.write(line & "\n") 276 | 277 | proc checkBuilderQueue(state: PState, platform: string) = 278 | ## Checks builder queue and sends a message to the builder immediatelly. 279 | if state.buildQueue.hasKey(platform) and 280 | state.buildQueue[platform].len != 0: 281 | let cm = state.buildQueue.mget(platform).pop() 282 | if not state.database.platformExists(cm.payload["payload"]["after"].str, 283 | platform): 284 | state.database.addPlatform(cm.payload["payload"]["after"].str, 285 | platform) 286 | let json = %{"payload": cm.payload["payload"], "rebuild": %false} 287 | var builder: TModule 288 | doAssert findBuilderModule(state, platform, builder) 289 | builder.sock.send($json & "\c\L") 290 | 291 | # TODO: Instead of using assertions provide a function which checks whether the 292 | # key exists and throw an exception if it doesn't. 293 | 294 | proc createGist(filename, content: string, description = "Nimbuild gist"): string {.raises: [].} = 295 | except: return "An error occurred creating gist: " & getCurrentExceptionMsg() 296 | var body = 297 | %{ "description": %description, 298 | "public": %false, 299 | "files": %{ 300 | filename: %{ 301 | "content": %content 302 | } 303 | } 304 | } 305 | let resp = httpclient.post("https://api.github.com/gists", body = $body, 306 | timeout = 2000) 307 | if resp.status.startsWith("201"): 308 | let respJson = resp.body.parseJSON() 309 | return respJson["html_url"].str 310 | else: 311 | return "Gist creation failed. Got status: " & resp.status 312 | 313 | proc refreshPackagesJson(state: PState) = 314 | let resp = httpclient.get("https://raw.githubusercontent.com/nimrod-code/" & 315 | "packages/master/packages.json", timeout = 2000) 316 | if resp.status.startsWith("200"): 317 | try: 318 | var test = parseJson(resp.body) 319 | state.packagesJson = base64.encode(resp.body) 320 | except: 321 | echo("Got incorrect packages.json, not saving.") 322 | echo(getCurrentExceptionMsg()) 323 | if state.packagesJson == nil: raise 324 | else: 325 | echo("Could not retrieve packages.json.") 326 | 327 | proc parseMessage(state: PState, mIndex: int, line: string) = 328 | var json = parseJson(line) 329 | var m = state.modules[mIndex] 330 | if json.existsKey("job"): 331 | # { job: TBuilderJob} 332 | # Change of a builder job. 333 | setJob(state, m.platform, TBuilderJob(json["job"].num)) 334 | 335 | elif json.existsKey("result"): 336 | let result = TResult(json["result"].num) 337 | let platf = state.platforms[m.platform] 338 | 339 | proc IRCInfo(): string = 340 | "[$1 $2 $3]" % [m.platform, platf.hash[0..11], platf.branch] 341 | 342 | let currentJob = jobInProgress(platf) 343 | case currentJob 344 | of jBuild: 345 | if result == Success: 346 | state.database.updateProperty(platf.hash, m.platform, "buildResult", 347 | $int(bSuccess)) 348 | state.IRCAnnounce(IRCInfo() & " Build OK.") 349 | else: 350 | assert json.existsKey("detail") 351 | state.database.updateProperty(platf.hash, m.platform, "buildResult", 352 | $int(bFail)) 353 | state.database.updateProperty(platf.hash, m.platform, "failReason", 354 | json["detail"].str) 355 | # This implies that the tests failed too. If we leave this as unknown, 356 | # the website will show the 'progress.gif' image, which we don't want. 357 | state.database.updateProperty(platf.hash, m.platform, 358 | "testResult", $int(tFail)) 359 | var important = false 360 | if platf.branch == "master": 361 | important = true 362 | state.IRCAnnounce("$1 Build failed: $2" % 363 | [IRCInfo(), json["detail"].str], important) 364 | of jTest: 365 | if result == Success: 366 | assert(json.existsKey("total")) 367 | assert(json.existsKey("passed")) 368 | assert(json.existsKey("skipped")) 369 | assert(json.existsKey("failed")) 370 | state.database.updateProperty(platf.hash, m.platform, 371 | "testResult", $int(tSuccess)) 372 | state.database.updateProperty(platf.hash, m.platform, 373 | "total", $json["total"].num) 374 | state.database.updateProperty(platf.hash, m.platform, 375 | "passed", $json["passed"].num) 376 | state.database.updateProperty(platf.hash, m.platform, 377 | "skipped", $json["skipped"].num) 378 | state.database.updateProperty(platf.hash, m.platform, 379 | "failed", $json["failed"].num) 380 | state.IRCAnnounce("$1 Test results: $2/$3." % 381 | [IRCInfo(), $json["passed"].num, $json["total"].num]) 382 | 383 | # Diff functionality 384 | if json.hasKey("diff") and json["diff"].kind == JArray: 385 | var succeedNow = "" 386 | var succeedNowCount = 0 387 | var failNow = "" 388 | var failNowCount = 0 389 | for i in 0 .. " & json["line"].str) 473 | of bFtpUploadSpeed: 474 | state.platforms.mget(m.platform).FTPSpeed = json["speed"].fnum 475 | of bEnd: 476 | # Close file 477 | state.modules[mIndex].logFile.close() 478 | 479 | # Build ended. Check queue for more builds awaiting. 480 | checkBuilderQueue(state, m.platform) 481 | of bStart: 482 | # Build started, open log file. 483 | assert json.existsKey("hash") 484 | assert json.existsKey("branch") 485 | let commitHash = json["hash"].str 486 | let commitBranch = json["branch"].str 487 | state.platforms.mget(m.platform).hash = commitHash 488 | state.platforms.mget(m.platform).branch = commitBranch 489 | let commitPath = state.jesterSettings.staticDir / "commits" / 490 | makeCommitPath(m.platform, commitHash) 491 | if not existsDir(commitPath.parentDir): 492 | createDir(commitPath.parentDir) 493 | if not existsDir(commitPath): 494 | createDir(commitPath) 495 | let logFilepath = state.jesterSettings.staticDir / "commits" / 496 | makeCommitPath(m.platform, commitHash) / "logs.txt" 497 | state.modules[mIndex].logFile = open(logFilepath, fmWrite) 498 | 499 | elif json.existsKey("payload"): 500 | # { "payload": { .. } } 501 | # Check if this is the Nim repo. 502 | if "nim-lang/nim" in json["payload"]["repository"]["url"].str.toLower(): 503 | # Check if the commit exists. 504 | if not state.database.commitExists(json["payload"]["after"].str): 505 | # Get the branch. 506 | let branch = json["payload"]["ref"].str[11 .. ^1] 507 | var commits = json["payload"]["commits"] 508 | var latestCommit = commits[commits.len-1] 509 | # Add commit to database 510 | state.database.addCommit(json["payload"]["after"].str, 511 | latestCommit["message"].str, 512 | latestCommit["author"]["username"].str, 513 | branch) 514 | 515 | # Send this message to the "builder" modules 516 | for module in items(state.modules): 517 | if module.name == "builder": 518 | # Check build queue. 519 | var toBuildQueue = false 520 | if state.buildQueue.hasKey(module.platform): 521 | toBuildQueue = state.buildQueue[module.platform].len > 0 522 | 523 | # Check if builder is currently building. 524 | if state.platforms[module.platform].isInProgress: 525 | toBuildQueue = true 526 | 527 | if not toBuildQueue: 528 | # Send immediately. 529 | state.database.addPlatform(json["payload"]["after"].str, 530 | module.platform) 531 | 532 | # Add "rebuild" flag. 533 | json["rebuild"] = newJBool(false) 534 | 535 | module.sock.send($json & "\c\L") 536 | else: 537 | var cm: TBQCommit 538 | cm.hash = json["payload"]["after"].str 539 | cm.branch = branch 540 | cm.payload = json 541 | if not state.buildQueue.hasKey(module.platform): 542 | state.buildQueue[module.platform] = @[] 543 | state.buildQueue.mget(module.platform).add(cm) 544 | 545 | else: 546 | echo("Commit already exists. Not rebuilding.") 547 | elif "nimrod-code/packages" in 548 | json["payload"]["repository"]["url"].str.toLower(): 549 | state.refreshPackagesJson() 550 | else: 551 | echo("Repo is not Nim. Got: " & 552 | json["payload"]["repository"]["url"].str) 553 | 554 | # Send this message to the "irc" module. 555 | if "irc" in state.modules: 556 | for module in items(state.modules): 557 | if module.name == "irc": 558 | module.sock.send($json & "\c\L") 559 | 560 | elif json.existsKey("rebuild"): 561 | # { "rebuild": "hash" } 562 | # TODO: Is this ever used? 563 | var hash = json["rebuild"].str 564 | var reply = newJObject() 565 | if state.database.commitExists(hash, true): 566 | # You can only rebuild the newest commit. (For now, TODO?) 567 | var fullHash = state.database.expandHash(hash) 568 | let branch = state.database.getBranch(fullHash) 569 | if state.database.isNewest(hash): 570 | var success = false 571 | for module in items(state.modules): 572 | if module.name == "builder": 573 | var jobj = newJObject() 574 | jobj["payload"] = newJObject() 575 | jobj["payload"]["after"] = newJString(fullHash) 576 | jobj["payload"]["ref"] = newJString("refs/heads/" & branch) 577 | jobj["rebuild"] = newJBool(true) 578 | 579 | if not state.database.platformExists(fullHash, module.platform): 580 | state.database.addPlatform(fullHash, module.platform) 581 | 582 | module.sock.send($jobj & "\c\L") 583 | success = true 584 | 585 | if success: 586 | reply["success"] = newJNull() 587 | else: 588 | reply["fail"] = newJString("No builders available.") 589 | else: 590 | reply["fail"] = newJString("Given commit is not newest.") 591 | else: 592 | reply["fail"] = newJString("Commit could not be found") 593 | 594 | m.sock.send($reply & "\c\L") 595 | 596 | elif json.existsKey("latestCommit"): 597 | let commit = state.database.getNewest() 598 | let branch = state.database.getBranch(commit) 599 | var reply = newJObject() 600 | reply["payload"] = newJObject() 601 | reply["payload"]["after"] = newJString(commit) 602 | reply["payload"]["ref"] = newJString("refs/heads/" & branch) 603 | reply["payload"]["commits"] = newJArray() 604 | reply["payload"]["commits"].add(%({"modified": %([%"build/csources.zip"])})) 605 | reply["rebuild"] = newJBool(true) 606 | m.sock.send($reply & "\c\L") 607 | if not state.database.platformExists(commit, m.platform): 608 | state.database.addPlatform(commit, m.platform) 609 | 610 | elif json.existsKey("pong"): 611 | # Module received PING and replied with PONG. 612 | state.modules[mIndex].pinged = false 613 | state.modules[mIndex].ping = epochTime() - json["pong"].str.parseFloat() 614 | state.modules[mIndex].lastPong = epochTime() 615 | 616 | elif json.existsKey("ping"): 617 | # Module thinks it's disconnected! Reply quickly! 618 | json["pong"] = json["ping"] 619 | json.delete("ping") 620 | m.sock.send($json & "\c\L") 621 | 622 | elif json.existsKey("do"): 623 | # { "do": "command to do (Info to get)" } 624 | if json["do"].str == "redisinfo": 625 | # This command asks the website for redis connection info. 626 | # { "redisinfo": { "port": ..., "password" } } 627 | var jobj = newJObject() 628 | jobj["redisinfo"] = newJObject() 629 | jobj["redisinfo"]["port"] = newJInt(state.redisPort) 630 | m.sock.send($jobj & "\c\L") 631 | else: 632 | echo("[Fatal] Can't understand message from " & m.name & ": ", 633 | line) 634 | assert(false) 635 | 636 | else: 637 | echo("[Fatal] Can't understand message from " & m.name & ": ", 638 | line) 639 | assert(false) 640 | 641 | proc handleModuleMsg(s: PAsyncSocket, arg: PObject) = 642 | var state = PState(arg) 643 | # Module sent a message to us 644 | var disconnect: seq[TModule] = @[] # Modules which disconnected 645 | for i in 0..state.modules.len()-1: 646 | template m: expr = state.modules[i] 647 | template onEOSDisconnect(operation: expr): stmt {.immediate.} = 648 | try: 649 | operation 650 | except EOS: 651 | disconnect.add(m()) 652 | continue 653 | 654 | if m.sock == s: 655 | var line = "" 656 | var ret = false 657 | onEOSDisconnect: 658 | ret = readLine(s, line) 659 | if ret: 660 | if line == "": 661 | disconnect.add(m) 662 | continue 663 | case m.status 664 | of MSConnecting: 665 | var errMsg = "" 666 | if state.parseGreeting(m, line, errMsg): 667 | onEOSDisconnect: 668 | m.sock.send($(%{ "reply": %"OK" }) & "\c\L") 669 | echo(uniqueMName(m), " accepted.") 670 | else: 671 | onEOSDisconnect: 672 | m.sock.send($(%{ "reply": %"FAIL", "reason": %errMsg }) & "\c\L") 673 | echo("Rejected ", uniqueMName(m)) 674 | disconnect.add(m) 675 | of MSConnected: 676 | echo("Got line from $1: $2" % [m.name, line]) 677 | 678 | # Getting a message is a sign of the module still being alive. 679 | state.modules[i].lastPong = epochTime() 680 | try: 681 | state.parseMessage(i, line) 682 | except: 683 | onEOSDisconnect: 684 | m.sock.send($(%{ 685 | "fatal": %getStackTrace(getCurrentException())}) & "\c\L") 686 | m.sock.close() 687 | echo("Fatal error for ", uniqueMName(m)) 688 | echo(getStackTrace(getCurrentException())) 689 | echo("--------------------------") 690 | IRCAnnounce(state, uniqueMName(m) & " created a fatal error.", true) 691 | disconnect.add(m) 692 | 693 | # Remove disconnected modules 694 | for m in items(disconnect): 695 | state.remove(m) 696 | 697 | proc handlePings(state: PState) = 698 | var remove: seq[TModule] = @[] # Modules that have timed out. 699 | for i in 0..state.modules.len-1: 700 | template module: expr = state.modules[i] 701 | var pingEvery = 100.0 702 | case module.status 703 | of MSConnected: 704 | if module.name == "builder": 705 | #if module.platform.startsWith("windows"): pingEvery = 15000.0 706 | 707 | if module.pinged and (epochTime() - module.lastPong) >= 25.0: 708 | echo(uniqueMName(module), 709 | " has not replied to PING. Assuming timeout!!!") 710 | remove.add(module) 711 | continue 712 | 713 | if (epochTime() - module.lastPong) >= pingEvery: 714 | var obj = newJObject() 715 | obj["ping"] = newJString(formatFloat(epochTime())) 716 | module.sock.send($obj & "\c\L") 717 | module.lastPong = epochTime() # This is a bit misleading, but I don't 718 | # want to add lastPing 719 | module.pinged = true 720 | echo("Pinging ", uniqueMName(module)) 721 | 722 | of MSConnecting: 723 | if (epochTime() - module.lastPong) >= 2.0: 724 | echo(uniqueMName(module), " did not send a greeting.") 725 | try: 726 | module.sock.send("{ \"reply\": \"FAIL\", \"desc\": \"Took too long\" }\c\L") 727 | except EOS: 728 | echo("Could not send error message for module: ", getCurrentExceptionMsg()) 729 | remove.add(module) 730 | 731 | # Remove the modules that have timed out. 732 | for m in items(remove): 733 | state.remove(m) 734 | 735 | # HTML Generation 736 | 737 | proc joinUrl(u, u2: string): string = 738 | if u.endswith("/"): 739 | return u & u2 740 | else: return u & "/" & u2 741 | 742 | proc getWebUrl(state: PState, c: TCommit, p: TPlatform): string = 743 | result = state.req.makeUri("commits/$2/$1/" % [c.hash[0..11], p.platform], 744 | absolute = false) 745 | 746 | proc getLogUrl(state: PState, c: TCommit, p: TPlatform): string = 747 | result = joinUrl(getWebUrl(state, c, p), "logs.txt") 748 | 749 | proc isBuilding(platforms: TTable[string, TStatus], p: string, c: TCommit): bool = 750 | return platforms[p].isInProgress and platforms[p].hash == c.hash 751 | 752 | proc genPlatformResult(state: PState, c: TCommit, p: TPlatform, 753 | platforms: TTable[string, TStatus], 754 | req: Request): string = 755 | result = "" 756 | case p.buildResult 757 | of bUnknown: 758 | # Check whether this platform is currently building this commit. 759 | if isBuilding(platforms, p.platform, c): 760 | result.add("" % 761 | [req.makeUri("static/images/progress.gif", absolute = false)]) 762 | of bFail: 763 | let logUrl = getLogUrl(state, c, p) 764 | result.add("fail" % [logUrl]) 765 | of bSuccess: result.add("ok") 766 | result.add(" ") 767 | case p.testResult 768 | of tUnknown: 769 | if isBuilding(platforms, p.platform, c): 770 | result.add("" % 771 | [req.makeUri("static/images/progress.gif", absolute = false)]) 772 | of tFail: 773 | let logUrl = getLogUrl(state, c, p) 774 | result.add("fail" % [logUrl]) 775 | of tSuccess: 776 | var testresultsURL = joinUrl(getWebUrl(state, c, p), "testresults.html") 777 | var percentage = float(p.passed) / float(p.total - p.skipped) * 100.0 778 | result.add("" % [testresultsURL] & 779 | formatFloat(percentage, precision=4) & "%") 780 | 781 | proc genBuildResult(state: PState, c: TCommit, p: TPlatform): string = 782 | result = "" 783 | case p.buildResult 784 | of bUnknown: 785 | # Check whether this platform is currently building this commit. 786 | if isBuilding(state.platforms, p.platform, c): 787 | result.add(htmlgen.`div`(class = "half indivUnknown", 788 | img(alt = "Building", 789 | src = state.req.makeUri("public/images/progress.gif", 790 | absolute = false)) 791 | )) 792 | else: 793 | result.add(htmlgen.`div`(class = "half indivUnknown", 794 | htmlgen.p("Unknown"))) 795 | of bFail: 796 | result.add(htmlgen.`div`(class = "half indivFailure", 797 | a(href = getLogUrl(state, c, p), class = "fail", "Fail") 798 | )) 799 | of bSuccess: 800 | result.add(htmlgen.`div`(class = "half indivSuccess", "OK")) 801 | 802 | proc genTestResult(state: PState, c: TCommit, p: TPlatform): string = 803 | result = "" 804 | case p.testResult 805 | of tUnknown: 806 | if isBuilding(state.platforms, p.platform, c): 807 | result.add(htmlgen.`div`(class = "half indivUnknown", 808 | img(alt = "Building", 809 | src = state.req.makeUri("public/images/progress.gif", 810 | absolute = false)) 811 | )) 812 | else: 813 | result.add(htmlgen.`div`(class = "half indivUnknown", 814 | htmlgen.p("Unknown"))) 815 | of tFail: 816 | result.add(htmlgen.`div`(class = "half indivFailure", 817 | a(href = getLogUrl(state, c, p), class = "fail", "Fail") 818 | )) 819 | of tSuccess: 820 | var testresultsURL = joinUrl(getWebUrl(state, c, p), "testresults.html") 821 | result.add(htmlgen.`div`(class = "half indivSuccess", 822 | a(href = testResultsURL, class = "success", 823 | $(p.passed) & "/" & $(p.total-p.skipped)) 824 | )) 825 | 826 | proc cmpPlatforms(a, b: string): int = 827 | if a == b: return 0 828 | var dashes = a.split('-') 829 | var dashes2 = b.split('-') 830 | if dashes[0] == dashes2[0]: 831 | if dashes[1] == dashes2[1]: return system.cmp(a,b) 832 | case dashes[1] 833 | of "x86": 834 | return 1 835 | of "x86_64": 836 | if dashes2[1] == "x86": return -1 837 | else: return 1 838 | of "ppc64": 839 | if dashes2[1] == "x86" or dashes2[1] == "x86_64": return -1 840 | else: return 1 841 | else: 842 | return system.cmp(dashes[1], dashes2[1]) 843 | else: 844 | case dashes[0] 845 | of "linux": 846 | return 1 847 | of "windows": 848 | if dashes2[0] == "linux": return -1 849 | else: return 1 850 | of "macosx": 851 | if dashes2[0] == "linux" or dashes2[0] == "windows": return -1 852 | else: return 1 853 | else: 854 | if dashes2[0] == "linux" or dashes2[0] == "windows" or 855 | dashes2[0] == "macosx": return -1 856 | else: 857 | return system.cmp(a, b) 858 | 859 | proc findLatestCommit(entries: seq[TEntry], 860 | platform: string, 861 | res: var tuple[entry: TEntry, latest: bool]): bool = 862 | result = false 863 | var i = 0 864 | for c, p in items(entries): 865 | if platform in p: 866 | let platf = p[platform] 867 | if platf.buildResult == bSuccess: 868 | res = ((c, p), i == 0 and entries[0].c.hash == c.hash) 869 | return true 870 | 871 | i.inc() 872 | 873 | proc genDownloadTable(req: Request, entries: seq[TEntry], 874 | platforms: seq[string]): string = 875 | result = "" 876 | 877 | var OSes: seq[string] = @[] 878 | var CPUs: seq[string] = @[] 879 | var versions: seq[tuple[ver: string, os: string]] = @[] 880 | for p in platforms: 881 | var spl = p.split('-') 882 | 883 | if spl.len() > 2: 884 | if (ver: spl[2], os: spl[0]) notin versions: 885 | versions.add((spl[2], spl[0])) 886 | elif spl[0] notin OSes: 887 | versions.add(("", spl[0])) 888 | 889 | if spl[0] notin OSes: OSes.add(spl[0]) 890 | if spl[1] notin CPUs: CPUs.add(spl[1]) 891 | 892 | var table = htmlhelp.initTable() 893 | table.addRow() 894 | table.addRow() # For Versions. 895 | table[0].addCol("", true) 896 | table[1].addCol("", true) 897 | for os in OSes: 898 | table[0].addCol(os, true) # Add OS. 899 | for cpuI, cpu in CPUs: 900 | if cpuI+2 > table.len()-1: table.addRow() 901 | table[2+cpuI].addCol(cpu, true) 902 | 903 | # Loop through versions. 904 | var currentVerI = 0 905 | 906 | while currentVerI < versions.len(): 907 | var columnAdded = false 908 | var pName = "" 909 | if versions[currentVerI].ver != "": 910 | pName = versions[currentVerI].os & "-" & 911 | cpu & "-" & versions[currentVerI].ver 912 | else: 913 | pName = versions[currentVerI].os & "-" & cpu 914 | 915 | if pName in platforms: 916 | var latestCommit: tuple[entry: TEntry, latest: bool] 917 | if entries.findLatestCommit(pName, latestCommit): 918 | var (entry, latest) = latestCommit 919 | var attrs: seq[tuple[name, content: string]] = @[] 920 | attrs.add(("class", if latest: "link green" else: "link orange")) 921 | if pName in entry.p: 922 | var weburl = req.makeUri("commits/$2/nimrod_$1.zip" % 923 | [entry.c.hash[0..11], entry.p[pName].platform], 924 | absolute = false) 925 | table[2+cpuI].addCol(a(entry.c.hash[0..11], href = weburl), attrs=attrs) 926 | columnAdded = true 927 | 928 | if not columnAdded: 929 | # Add an empty column. 930 | table[2+cpuI].addCol("") 931 | 932 | currentVerI.inc() 933 | 934 | for v in versions: 935 | table[1].addCol(v.ver, true) 936 | if v.ver != "": 937 | # Add +1 to colspan of OS 938 | var cols = findCols(table[0], v.os) 939 | assert cols.len > 0 940 | if not cols[0].attrs.hasKey("colspan"): 941 | cols[0].attrs["colspan"] = "1" 942 | else: 943 | cols[0].attrs["colspan"] = $(cols[0].attrs["colspan"].parseInt + 1) 944 | 945 | result = table.toHtml("id=\"downloads\"") 946 | 947 | proc genTopButtons(req: Request, platforms: TTable[string, TStatus], 948 | entries: seq[TEntry]): string = 949 | # Generate buttons for C sources and docs. 950 | # Find the latest C sources. 951 | result = "" 952 | var csourceWeb = "" 953 | var csourceFound = false 954 | var csourceLatest = false 955 | var i = 0 956 | for c, p in items(entries): 957 | for platf in p: 958 | if platf.csources: 959 | csourceWeb = req.makeUri("commits/$2/nimrod_$1_csources.zip" % 960 | [c.hash[0..11], platf.platform], absolute=false) 961 | csourceFound = true 962 | csourceLatest = i == 0 963 | break 964 | if csourceFound: break 965 | i.inc() 966 | 967 | # Find out whether latest doc gen succeeded. 968 | var docgenSuccess = true # By default it succeeded. 969 | for p, s in pairs(platforms): 970 | if s.jobs[jDocGen] == jFail: 971 | docgenSuccess = false 972 | break 973 | 974 | var csourceClass = "right " & (if csourceLatest: "active" else: "warning") & 975 | " button" 976 | var docClass = "left " & (if docgenSuccess: "active" else: "warning") & 977 | " button" 978 | 979 | var docWeb = req.makeUri("docs/lib.html", absolute=false) 980 | 981 | result.add(a(span("", class = "download") & 982 | span("C Sources", class = "platform"), 983 | class = csourceClass, href = csourceWeb)) 984 | 985 | result.add(a(span("", class = "book") & 986 | span("Documentation", class = "platform"), 987 | class = docClass, href = docWeb)) 988 | 989 | proc genCommitUrl(hash: string): string = 990 | return joinUrl("https://github.com/Araq/Nimrod/commit/", hash) 991 | 992 | proc genUserUrl(user: string): string = 993 | return joinUrl("https://github.com/", user) 994 | 995 | proc genSpecificBranchHTML(state: PState, branch: string, 996 | info: tuple[c: TCommit, buildInfo: TPlatform]): string = 997 | let (commit, build) = (info.c, info.buildInfo) 998 | const month = 2_628_000 999 | let dateClass = "date " & 1000 | (if (commit.date - getTime()) > month: "outdated" else: "") 1001 | result = 1002 | htmlgen.`div`(class = "lastResults", 1003 | htmlgen.`div`(class = "branch " & (if branch == "master": "master" else: ""), 1004 | span(title="Branch tested", branch) 1005 | ), 1006 | state.genBuildResult(commit, build), 1007 | state.genTestResult(commit, build), 1008 | p(a(href = genCommitUrl(commit.hash),commit.hash[0..11]), " by ", 1009 | a(href=genUserUrl(commit.username), commit.username), " (", 1010 | a(href=getLogUrl(state, commit, build), "logs"), ")" 1011 | ), 1012 | p(class = "commitMsg", commit.commitMsg), 1013 | p(class = dateClass, $(commit.date)) 1014 | ) 1015 | 1016 | proc genSpecificBuilderHTML(state: PState, 1017 | platfName: string): tuple[inProgress: bool, html: string] = 1018 | result = (false, "") 1019 | let imgProgress = "\"Busy\"" % 1020 | [state.req.makeUri("images/progress.gif", absolute = false)] 1021 | var builderModule: TModule 1022 | if findBuilderModule(state, platfName, builderModule): 1023 | let job = state.platforms[platfName] 1024 | let lag = int(builderModule.ping * 1000.0) 1025 | var lagTxt = "" 1026 | if lag == 0: 1027 | lagTxt = "<0ms" 1028 | else: 1029 | lagTxt = $lag & "ms" 1030 | 1031 | var progressSpecific = "" 1032 | if job.isInProgress: 1033 | progressSpecific.add imgProgress 1034 | let masterSpecific = if job.branch == "master": "master" else: "" 1035 | progressSpecific.add p("Current: " & job.hash[0..11] & " (" & 1036 | span(class="branch " & masterSpecific, job.branch) & 1037 | ")") 1038 | var queueSpecific = "" 1039 | if state.buildQueue.hasKey(platfName): 1040 | let q = state.buildQueue[platfName] 1041 | if q.len != 0: 1042 | queueSpecific = p($q.len & " commits in build queue") 1043 | 1044 | result.html = htmlgen.`div`(class = "buildInfo", 1045 | progressSpecific, 1046 | p($job), 1047 | p(lagTxt), 1048 | queueSpecific 1049 | ) 1050 | result.inProgress = job.isInProgress 1051 | else: 1052 | result.html = htmlgen.`div`(class = "buildInfo", 1053 | p("Builder not connected.")) 1054 | 1055 | proc genBuildResults(state: PState, platforms: seq[string], entr: seq[TEntry]): string = 1056 | # Platform name -> [branch, html generated] 1057 | var platformBuilds = initTable[ 1058 | string, 1059 | TTable[string, tuple[c: TCommit, buildInfo: TPlatform]]]() 1060 | 1061 | # TODO: Move to MongoDB and a better more efficient db layout. 1062 | # the following code is extremely slow and complicated. 1063 | 1064 | # The following sorts the commits into a list more suited to the new layout. 1065 | for entry in items(entr): 1066 | let (commit, builds) = (entry.c, entry.p) 1067 | for build in builds: 1068 | if isBuilding(state.platforms, build.platform, commit): 1069 | continue # If the builder is currently building it, don't show it here. 1070 | if build.buildResult == bUnknown: 1071 | continue # No point in showing an unknown for both build&test result. 1072 | # So skip it. 1073 | if not platformBuilds.hasKey(build.platform): 1074 | platformBuilds[build.platform] = initTable[ 1075 | string, 1076 | tuple[c: TCommit, buildInfo: TPlatform]]() 1077 | let thisBranch = (if isNil(commit.branch): "master" else: commit.branch) 1078 | assert thisBranch != "" 1079 | if platformBuilds[build.platform].hasKey(thisBranch): 1080 | # Already got the latest commit for this branch 1081 | continue 1082 | platformBuilds.mget(build.platform)[thisBranch] = (c: commit, buildInfo: build) 1083 | 1084 | # platfClass = 1085 | # If build in progress: blue (Progress) 1086 | # If master branch failed: red (fail) 1087 | # If master branch tests not 100%: orange 1088 | # If master branch fully successful: green. 1089 | 1090 | proc genPlatfBuildRes(state: PState, class, name, 1091 | branches, builderStatus: string): string = 1092 | result = htmlgen.`div`(class="platfBuildResult " & class, 1093 | htmlgen.`div`(class="header", span(name)), 1094 | branches, 1095 | htmlgen.`div`(class="header", span("Builder status")), 1096 | builderStatus) 1097 | 1098 | result = "" 1099 | # 3 platforms per single row, only needed to keep the boxes in one single row 1100 | # ... layout fix basically. 1101 | var platfsCol = "" 1102 | var platfsCount = 0 1103 | for platfName in platforms: 1104 | if not platformBuilds.hasKey(platfName): 1105 | let (inProgress, html) = genSpecificBuilderHTML(state, platfName) 1106 | if inProgress: 1107 | result.add genPlatfBuildRes(state, "platfProgress", platfName, "", html) 1108 | continue 1109 | else: 1110 | continue # No commits were built for this, and nothing is building. 1111 | 1112 | let value = platformBuilds[platfName] 1113 | var branches = "" 1114 | if value.hasKey("master"): 1115 | branches.add(genSpecificBranchHTML(state, "master", value["master"])) 1116 | for branch, info in value: 1117 | if branch == "master": continue 1118 | branches.add(genSpecificBranchHTML(state, branch, info)) 1119 | 1120 | let (inProgress, builderHtml) = genSpecificBuilderHTML(state, platfName) 1121 | var platfClass = "platfWarning" 1122 | if inProgress: platfClass = "platfProgress" 1123 | if value.hasKey("master"): 1124 | if value["master"].buildInfo.buildResult == bFail or 1125 | value["master"].buildInfo.testResult == tFail: 1126 | platfClass = "platfFailure" 1127 | elif value["master"].buildInfo.testResult == tSuccess and 1128 | value["master"].buildInfo.failed == 0: 1129 | platfClass = "platfSuccess" 1130 | else: 1131 | platfClass = "platfWarning" 1132 | else: 1133 | platfClass = "platfWarning" # Just to be explicit. 1134 | 1135 | platfsCol.add genPlatfBuildRes(state, platfClass, platfName, branches, builderHtml) 1136 | inc(platfsCount) 1137 | 1138 | if platfsCount == 3: 1139 | result.add(htmlgen.`div`(style="float: left; width: 100%;", platfsCol)) 1140 | platfsCount = 0 1141 | platfsCol = "" 1142 | # Add the rest of the rows. 1143 | result.add(htmlgen.`div`(style="float: left; width: 100%;", platfsCol)) 1144 | 1145 | 1146 | include "index.html" 1147 | # Jester 1148 | 1149 | proc cleanup(state: PState) = 1150 | echo("^C detected. Cleaning up...") 1151 | for m in items(state.modules): 1152 | # TODO: Send something to the modules to warn them? 1153 | m.sock.close() 1154 | 1155 | proc handleAccept(s: PAsyncSocket, state: PState) = 1156 | # Connection from a module 1157 | var client: PAsyncSocket; new(client) 1158 | var IPAddr = "" 1159 | s.acceptAddr(client, IPAddr) 1160 | state.addModule(client, IPAddr) 1161 | 1162 | when isMainModule: 1163 | var configPath = "" 1164 | if paramCount() > 0: 1165 | configPath = paramStr(1) 1166 | echo("Loading config ", configPath, "...") 1167 | else: 1168 | quit("Usage: ./website configPath") 1169 | 1170 | echo("Started website: built at ", CompileDate, " ", CompileTime) 1171 | 1172 | var state = website.open(configPath) 1173 | var settings = newSettings(port = net.Port(state.scgiPort)) 1174 | state.jesterSettings = settings 1175 | 1176 | routes: 1177 | get "/": 1178 | state.req = request 1179 | let html = state.genHtml() 1180 | resp html 1181 | 1182 | while true: 1183 | doAssert state.dispatcher.poll(1) 1184 | 1185 | asyncdispatch.poll(1) 1186 | 1187 | state.handlePings() 1188 | 1189 | state.database.keepAlive() 1190 | 1191 | 1192 | -------------------------------------------------------------------------------- /src/website.nim.cfg: -------------------------------------------------------------------------------- 1 | -d:ssl -------------------------------------------------------------------------------- /structure.markdown: -------------------------------------------------------------------------------- 1 | # Structure 2 | 3 | Github 4 | \ 5 | \ 6 | Hub --------- Builder A, B, C 7 | \ 8 | \ 9 | NimBot 10 | 11 | ## The Hub a.k.a "the website" 12 | 13 | This serves the website, it also acts as a hub, all the "modules" connect to it. 14 | 15 | ## Builder 16 | 17 | There are many of these connected at once to the Hub. Each runs on a different 18 | platform. 19 | 20 | Its job is to wait for a new commit notification from the hub. When this event 21 | occurs, it will update the local Nimrod git repository and begin the build 22 | process. 23 | 24 | The build process consists of the following: 25 | 26 | * Downloading the current C sources and building from them if they changed. 27 | * Bootstrapping the Nimrod compiler in debug and release mode. 28 | * Running the test suite. 29 | * Building the C sources (one builder only) 30 | * Building the documentation (one builder only) 31 | 32 | While doing so the builder keeps the hub updated with the progress of the build. 33 | The build is separated into jobs which include ``JBuild``, ``JTest``, 34 | ``JCSrcGen``, ``JDocGen`` and ``JInnoGen``. The jobs are not run in parallel, 35 | and if one fails the rest fail. 36 | 37 | ## Github 38 | 39 | This module waits for a request from Github which notifies it that there is a 40 | new commit in Nimrod's repo. It passes this information on to the hub. 41 | 42 | The hub uses this information to do multiple things including: 43 | 44 | * Sending the information to NimBot so that it announces the commit in the IRC channel. 45 | * Notifying the connected builders that a new commit is ready to be built. 46 | 47 | ## NimBot 48 | 49 | This is the IRC bot that resides in #nimrod on Freenode. It announces new commits 50 | to the Nimrod repo (and other nimrod-related repos) in that channel. It also 51 | announces build info in the #nimbuild channel. 52 | 53 | Other features include: 54 | 55 | * !seen feature 56 | 57 | ## Communication 58 | 59 | The hub communicates with the modules that are connected to it using JSON and 60 | vice versa; the modules do the same. 61 | 62 | ### Hub 63 | 64 | The hub currently supports the following messages: 65 | 66 | #### ``{ "job": types.TBuilderJob.ord }`` 67 | 68 | This marks the start of a new build job. 69 | 70 | **Sent by:** builder. 71 | 72 | #### ``{ "result": system.TResult.ord }`` 73 | 74 | Finishes the current builder's job with ``result`` (either success or failure). 75 | 76 | Other params: 77 | 78 | * ``detail`` - When ``result`` is ``Failure`` this contains the reason as to the 79 | failure. 80 | * ``total``/``passed``/``skipped``/``failed`` - When the current job is ``JTest`` 81 | and ``result`` is ``Success`` these fields contain the total tests, and also 82 | the amount of passed, skipped and failed tests. 83 | 84 | **Sent by:** builder. 85 | 86 | #### ``{ "eventType": TBuilderEventType.ord }`` 87 | 88 | Updates the hub on the status of a build in progress. 89 | 90 | **Sent by:** builder. 91 | 92 | #### ``{ "payload": { ... } }`` 93 | 94 | Tells the hub about a new commit or multiple commits made to a repo. 95 | Not necessarily the Nimrod repo. 96 | 97 | **Sent by:** github 98 | 99 | #### ``{ "latestCommit": true }`` 100 | 101 | Requests info about the latest commit from the hub. 102 | 103 | **Sent by:** builder. 104 | 105 | #### ``{ "ping": TimeSinceUnixEpoch }`` 106 | 107 | A module is verifying that it's still connected. 108 | 109 | **Sent by:** All modules. 110 | 111 | #### ``{ "pong": TimeSinceUnixEpoch }`` 112 | 113 | A module is replying to a "ping" message from the hub. 114 | 115 | **Sent by:** All modules. 116 | 117 | #### ``{ "do": "request" }`` 118 | 119 | A module is asking for info. 120 | 121 | ##### ``redisinfo`` 122 | 123 | Sends redis db info to the module requesting it. 124 | 125 | **Sent by:** NimBot. 126 | 127 | # Database 128 | 129 | The current database that is being used is redis. 130 | 131 | ## Structure 132 | 133 | Here is a brief description of the current database which is to be deprecated. 134 | 135 | ### A list 136 | 137 | This will be called ``commits``. Each commit hash will be LPUSH-ed onto this list for easy iteration from the latest to the oldest commits with LRANGE. 138 | 139 | ### Keys 140 | 141 | Information about each commit will be saved in a hash by the name of ``commit_hash``. 142 | The fields will be: 143 | * commitMsg 144 | * date 145 | * username 146 | * branch 147 | 148 | Specific information about a build can be retrieved by accessing ``platform:commit_hash``, where ``platform`` can be for example "linux-x86". 149 | The fields that these will contain will be: 150 | * buildResult -> db.TBuildResult ( bUnknown, bFail, bSuccess ) 151 | * testResult -> db.TTestResult ( tUnknown, tFail, tSuccess ) 152 | * total -> total tests 153 | * passed -> passed tests 154 | * skipped -> skipped tests 155 | * failed -> failed tests 156 | * csources -> whether the csources have been built for this platform/commit combination ("t" or "f" (?)) 157 | * timeBuild -> Time taken to build TODO 158 | * timeTest -> Time taken to test 159 | 160 | -------------------------------------------------------------------------------- /tests/dummyhub.nim: -------------------------------------------------------------------------------- 1 | import asyncio, jester, sockets, json 2 | 3 | var currentClient: PAsyncSocket 4 | 5 | proc clientRead(s: PAsyncSocket) = 6 | var line = "" 7 | if s.readLine(line): 8 | if line == "": 9 | echo("Client disconnected") 10 | currentClient.close() 11 | currentClient = nil 12 | else: 13 | echo("Recv: ", line) 14 | var json = parseJson(line) 15 | if json.hasKey("ping"): 16 | json["pong"] = json["ping"] 17 | json.delete("ping") 18 | s.send($json & "\c\L") 19 | elif json.hasKey("name"): 20 | s.send($(%{ "reply": %"OK" }) & "\c\L") 21 | 22 | when isMainModule: 23 | var disp = newDispatcher() 24 | var hubSock = AsyncSocket() 25 | hubSock.bindAddr(TPort(5123)) 26 | hubSock.listen() 27 | 28 | hubSock.handleAccept = 29 | proc (s: PAsyncSocket) = 30 | if currentClient != nil: currentClient.close() 31 | new(currentClient) 32 | s.accept(currentClient) 33 | currentClient.handleRead = clientRead 34 | disp.register(currentClient) 35 | disp.register(hubSock) 36 | 37 | get "/": 38 | if currentClient == nil: resp "No client." 39 | else: resp "OK" 40 | 41 | get "/boot": 42 | if currentClient == nil: halt "No client! :(" 43 | var reply = newJObject() 44 | reply["payload"] = newJObject() 45 | reply["payload"]["after"] = newJString("HEAD") 46 | reply["payload"]["ref"] = newJString("refs/heads/master") 47 | reply["payload"]["commits"] = newJArray() 48 | reply["rebuild"] = newJBool(true) 49 | currentClient.send($reply & "\c\L") 50 | resp "We are now bootstrapping." 51 | 52 | get "/stop": 53 | if currentClient == nil: halt "No client! :(" 54 | var reply = %{"do": %"stop"} 55 | currentClient.send($reply & "\c\L") 56 | resp "Stopped." 57 | 58 | disp.register() 59 | 60 | while true: 61 | doAssert disp.poll() 62 | -------------------------------------------------------------------------------- /todo.markdown: -------------------------------------------------------------------------------- 1 | * Move from redis to mongodb or sqlite(?) 2 | * Convert redis data to suit the current website layout. 3 | * Hub should only have the FTP info, builders should query it for this data. 4 | * Download table is broken. 5 | * Keep only the latest version of Nimrod for download. 6 | * grep "TODO" 7 | * Fix nimbot 8 | * Integration with Github status API 9 | * When a pull request is made nimbuild should be intelligent on what it compiles 10 | I.e. If the file edited is in the compiler/ directory then nimrod should be bootstrapped 11 | if the file edited is a test, the test should be ran 12 | if the file edited is a module in the stdlib it should just be compiled 13 | * This should then be reported to github... 14 | 15 | * Nimbuild should be smart, as described above, this should be replicated to 16 | all pushes. Single file change: build only that file, compiler changes do whole bootstrap + test suite. 17 | etc. 18 | * If the newest build only built one file, the website should show the last known test results (perhaps with a warning). 19 | * DB needs to know about this. 20 | 21 | * Inspect diff. 22 | * If all lines that were changed start with (\s+)## then no need to bootstrap, 23 | just ened to rebuild the docs. 24 | 25 | * Change the left side color of each platform box to show the current builder status. 26 | * Pulsates blue when building 27 | * Red when disconnected 28 | * Blue when connected? 29 | 30 | * .deb gen --------------------------------------------------------------------------------