unofficial mirror of guix-patches@gnu.org 
 help / color / mirror / code / Atom feed
blob 62179184fafb3485fe28054395ee8fc9a9241911 18166 bytes (raw)
name: src/cuirass/remote-server.scm 	 # note: path name is non-authoritative(*)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
 
;;; remote-server.scm -- Remote build server.
;;; Copyright © 2020 Mathieu Othacehe <othacehe@gnu.org>
;;;
;;; This file is part of Cuirass.
;;;
;;; GNU Guix is free software; you can redistribute it and/or modify it
;;; under the terms of the GNU General Public License as published by
;;; the Free Software Foundation; either version 3 of the License, or (at
;;; your option) any later version.
;;;
;;; GNU Guix is distributed in the hope that it will be useful, but
;;; WITHOUT ANY WARRANTY; without even the implied warranty of
;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;;; GNU General Public License for more details.
;;;
;;; You should have received a copy of the GNU General Public License
;;; along with GNU Guix.  If not, see <http://www.gnu.org/licenses/>.

(define-module (cuirass remote-server)
  #:use-module (cuirass base)
  #:use-module (cuirass remote)
  #:use-module (gcrypt pk-crypto)
  #:use-module (guix avahi)
  #:use-module (guix base32)
  #:use-module (guix base64)
  #:use-module (guix config)
  #:use-module (guix derivations)
  #:use-module (guix records)
  #:use-module (guix packages)
  #:use-module (guix pki)
  #:use-module (guix scripts)
  #:use-module (guix store)
  #:use-module (guix ui)
  #:use-module (guix workers)
  #:use-module (guix build download)
  #:use-module (guix build syscalls)
  #:use-module (gcrypt hash)
  #:use-module (gcrypt pk-crypto)
  #:use-module (simple-zmq)
  #:use-module (rnrs bytevectors)
  #:use-module (srfi srfi-1)
  #:use-module (srfi srfi-26)
  #:use-module (srfi srfi-37)
  #:use-module (ice-9 atomic)
  #:use-module (ice-9 match)
  #:use-module (ice-9 q)
  #:use-module (ice-9 rdelim)
  #:use-module (ice-9 regex)
  #:use-module (ice-9 threads)

  #:export (remote-server))

;; Indicate if the process has to be stopped.
(define %stop-process?
  (make-atomic-box #f))

;; Whether to add build items to the store.
(define %add-to-store?
  (make-parameter #f))

(define %cache-directory
  (make-parameter #f))

(define %private-key
  (make-parameter #f))

(define %public-key
  (make-parameter #f))

(define service-name
  "Cuirass remote server")

(define (show-help)
  (format #t (G_ "Usage: remote-server [OPTION]...
Start a remote build server.\n"))
  (display (G_ "
  -a, --add-to-store        register built items to the store"))
  (display (G_ "
  -b, --backend-port=PORT   listen worker connections on PORT"))
  (display (G_ "
  -p, --publish-port=PORT   publish substitutes on PORT"))
  (display (G_ "
  -c, --cache=DIRECTORY     cache built items to DIRECTORY"))
  (display (G_ "
      --public-key=FILE     use FILE as the public key for signatures"))
  (display (G_ "
      --private-key=FILE    use FILE as the private key for signatures"))
  (newline)
  (display (G_ "
  -h, --help                display this help and exit"))
  (display (G_ "
  -V, --version             display version information and exit"))
  (newline)
  (show-bug-report-information))

(define %options
  (list (option '(#\h "help") #f #f
                (lambda _
                  (show-help)
                  (exit 0)))
        (option '(#\V "version") #f #f
                (lambda _
                  (show-version-and-exit "guix publish")))
        (option '(#\a "add-to-store") #t #f
                (lambda (opt name arg result)
                  (alist-cons 'add-to-store? arg result)))
        (option '(#\b "backend-port") #t #f
                (lambda (opt name arg result)
                  (alist-cons 'backend-port (string->number* arg) result)))
        (option '(#\p "publish-port") #t #f
                (lambda (opt name arg result)
                  (alist-cons 'publish-port (string->number* arg) result)))
        (option '(#\c "cache") #t #f
                (lambda (opt name arg result)
                  (alist-cons 'cache arg result)))
        (option '("public-key") #t #f
                (lambda (opt name arg result)
                  (alist-cons 'public-key-file arg result)))
        (option '("private-key") #t #f
                (lambda (opt name arg result)
                  (alist-cons 'private-key-file arg result)))))

(define %default-options
  `((backend-port . 5555)
    (publish-port . 5556)
    (public-key-file . ,%public-key-file)
    (private-key-file . ,%private-key-file)))

\f
;;;
;;; Build workers.
;;;

(define %workers
  ;; Set of connected workers.
  (make-hash-table))

(define %build-queues
  ;; Builds request queue.
  (map (lambda (system)
         (cons system (make-q)))
       %supported-systems))

(define (find-system-queues systems)
  "Return the list of build queues for SYSTEMS that are not empty."
  (filter-map (match-lambda
                ((system . queue)
                 (and (member system systems)
                      (not (q-empty? queue))
                      queue)))
              %build-queues))

(define (build-available? name)
  "Return #t if there is some available work for the worker with the given
NAME and #f otherwise."
  (let* ((worker (hash-ref %workers name))
         (systems (worker-systems worker))
         (queues (find-system-queues systems)))
    (not (null? queues))))

(define (pop-random-build name)
  "Pop randomly and return a build from all the build queues with available
work for the worker with the given NAME."
  (define (random-queue queues)
    (list-ref queues (random (length queues))))

  (let* ((worker (hash-ref %workers name))
         (systems (worker-systems worker))
         (queues (find-system-queues systems)))
    (q-pop! (random-queue queues))))

(define* (read-client-exp client exp)
  "Read the given EXP sent by CLIENT."
  (catch 'system-error
    (lambda ()
      (match (zmq-read-message exp)
        (('build ('drv drv) ('system system))
         (let ((system (or system
                           (derivation-system
                            (read-derivation-from-file drv)))))
           ;; Push the derivation to the matching queue according to the
           ;; targeted system. Also save the client ID in the queue to be able
           ;; to send it build events later on.
           (q-push! (assoc-ref %build-queues system)
                    (list client drv))))))
    (const #f)))

(define* (read-worker-exp exp #:key reply-worker)
  "Read the given EXP sent by a worker.  REPLY-WORKER is a procedure that can
be used to reply to the worker."
  (match (zmq-read-message exp)
    (('worker-ready worker)
     (let* ((worker* (sexp->worker worker))
            (name (worker-name worker*)))
       (info (G_ "Worker `~a' is ready.~%") name)
       (hash-set! %workers name worker*)))
    (('worker-request-work name)
     (if (build-available? name)
         (match (pop-random-build name)
           ((client drv)
            (reply-worker client (zmq-build-request-message drv))))
         (reply-worker
          (zmq-empty-delimiter)
          (zmq-no-build-message))))))

\f
;;;
;;; Fetch workers.
;;;

(define (zmq-fetch-workers-endpoint)
  "inproc://fetch-workers")

(define (zmq-fetch-worker-socket)
  "Return a socket used to communicate with the fetch workers."
  (let ((socket (zmq-create-socket %zmq-context ZMQ_DEALER))
        (endpoint (zmq-fetch-workers-endpoint)))
    (zmq-connect socket endpoint)
    socket))

(define (strip-store-prefix file)
  ; Given a file name like "/gnu/store/…-foo-1.2/bin/foo", return
  ;; "/bin/foo".
  (let* ((len  (string-length %store-directory))
         (base (string-drop file (+ 1 len))))
    (match (string-index base #\/)
      (#f    base)
      (index (string-drop base index)))))

(define (publish-nar-url publish-url store-hash)
  "Return the URL of STORE-HASH nar substitute on PUBLISH-URL."
  (format #f "~a/nar/gzip/~a" publish-url store-hash))

(define (publish-narinfo-url publish-url store-hash)
  "Return the URL of STORE-HASH narinfo file on PUBLISH-URL."
  (let ((hash (and=> (string-index store-hash #\-)
                     (cut string-take store-hash <>))))
    (format #f "~a/~a.narinfo" publish-url hash)))

(define (nar-path cache-directory output)
  "Return the path of the NAR file for OUTPUT in CACHE-DIRECTORY."
  (string-append cache-directory "/" (basename output) ".nar"))

(define (narinfo-path cache-directory output)
  "Return the path of the NARINFO file for OUTPUT in CACHE-DIRECTORY."
  (string-append cache-directory "/" (basename output) ".narinfo"))

(define* (sign-narinfo! narinfo)
  "Edit the given NARINFO file to replace the worker signature by the remote
build server signature."
  (define (signed-string s)
    (let* ((hash (bytevector->hash-data (sha256 (string->utf8 s))
                                        #:key-type (key-type (%public-key)))))
      (signature-sexp hash (%private-key) (%public-key))))

  (define base64-encode-string
    (compose base64-encode string->utf8))

  (define lines
    (call-with-input-file narinfo
      (lambda (port)
        (let loop ((line (read-line port))
                   (lines '()))
          (if (eof-object? line)
              (reverse lines)
              (loop (read-line port)
                    (cons line lines)))))))
  (let* ((lines
          (filter (lambda (line)
                    (not (string-match "^Signature:" line)))
                  lines))
         (info (format #f "~a~%" (string-join lines "\n")))
         (signature (base64-encode-string
                     (canonical-sexp->string (signed-string info)))))
    (call-with-output-file narinfo
      (lambda (port)
        (format port "~aSignature: 1;~a;~a~%"
                info (gethostname) signature)))))

(define (download-nar cache-directory outputs url)
  "Download in CACHE-DIRECTORY the OUTPUTS from the substitute server at URL."
  (for-each
   (lambda (output)
     (let* ((path (derivation-output-path output))
            (store-hash (strip-store-prefix path))
            (nar-file (nar-path cache-directory store-hash))
            (narinfo-file (narinfo-path cache-directory store-hash))
            (nar-url (publish-nar-url url store-hash))
            (narinfo-url (publish-narinfo-url url store-hash)))
       (unless (file-exists? nar-file)
         (url-fetch nar-url nar-file))

       (unless (file-exists? narinfo-file)
         (url-fetch narinfo-url narinfo-file)
         (sign-narinfo! narinfo-file))))
   outputs))

(define (add-to-store outputs url)
  "Add the OUTPUTS that are available from the substitute server at URL to the
store."
  (with-store store
    (for-each (lambda (output)
                (add-substitute-url store url)
                (ensure-path store output))
              (map derivation-output-path outputs))))

(define (need-fetching? message)
  "Return #t if the received MESSAGE implies that some output fetching is
required and #f otherwise."
  (match (zmq-read-message message)
    (('build-succeeded ('drv drv) ('url url))
     #t)
    (else #f)))

(define* (run-fetch message #:key reply)
  "Read MESSAGE and download the corresponding build outputs.  If
%CACHE-DIRECTORY is set, download the matching NAR and NARINFO files in this
directory.  If %ADD-TO-STORE? is set, add the build outputs to the store.

REPLY is procedure used to forward MESSAGE to the client once the build
outputs are downloaded."
  (define (build-outputs drv)
    (catch 'system-error
      (lambda ()
        (map (match-lambda
               ((output-name . output)
                output))
             (derivation-outputs
              (read-derivation-from-file drv))))
      (const '())))

  (match (zmq-read-message message)
    (('build-succeeded ('drv drv) ('url url))
     (let ((outputs (build-outputs drv)))
       (when (%add-to-store?)
         (add-to-store outputs url))
       (when (%cache-directory)
         (download-nar (%cache-directory) outputs url))
       (reply message)))))

(define (start-fetch-worker name)
  "Start a fetch worker thread with the given NAME.  This worker takes care of
downloading build outputs.  It communicates with the remote server using a ZMQ
socket."
  (define (reply socket client)
    (lambda (message)
      (zmq-send-msg-parts-bytevector
       socket
       (list client (zmq-empty-delimiter) (string->bv message)))))

  (call-with-new-thread
   (lambda ()
     (set-thread-name name)
     (let ((socket (zmq-fetch-worker-socket)))
       (let loop ()
         (match (zmq-get-msg-parts-bytevector socket '())
           ((client empty rest)
            (let ((message (bv->string rest)))
              (run-fetch (bv->string rest)
                         #:reply (reply socket client)))))
         (loop))))))

\f
;;;
;;; ZMQ connection.
;;;

(define %zmq-context
  (zmq-create-context))

(define (zmq-backend-endpoint backend-port)
  "Return a ZMQ endpoint string allowing TCP connections on BACKEND-PORT from
all network interfaces."
  (string-append "tcp://*:" (number->string backend-port)))

(define (zmq-start-proxy backend-port)
  "This procedure starts a proxy between client connections from the IPC
frontend to the workers connected through the TCP backend."
  (define (socket-ready? items socket)
    (find (lambda (item)
            (eq? (poll-item-socket item) socket))
          items))

  (let* ((client-socket
          (zmq-create-socket %zmq-context ZMQ_ROUTER))
         (build-socket
          (zmq-create-socket %zmq-context ZMQ_ROUTER))
         (fetch-socket
          (zmq-create-socket %zmq-context ZMQ_DEALER))
         (poll-items (list
                      (poll-item client-socket ZMQ_POLLIN)
                      (poll-item build-socket ZMQ_POLLIN)
                      (poll-item fetch-socket ZMQ_POLLIN))))

    (zmq-bind-socket client-socket (zmq-frontend-endpoint))
    (zmq-bind-socket build-socket (zmq-backend-endpoint backend-port))
    (zmq-bind-socket fetch-socket (zmq-fetch-workers-endpoint))

    ;; Change frontend socket permissions.
    (chmod (zmq-frontend-socket-name) #o666)

    ;; Do not use the built-in zmq-proxy as we want to edit the envelope of
    ;; frontend messages before forwarding them to the backend.
    (let loop ()
      (let ((items (zmq-poll* poll-items)))
        ;; CLIENT -> REMOTE-SERVER.
        (when (zmq-socket-ready? items client-socket)
          (match (zmq-get-msg-parts-bytevector client-socket)
            ((client empty rest)
             (read-client-exp client (bv->string rest)))))
        ;; BUILD-WORKER -> REMOTE-SERVER.
        (when (zmq-socket-ready? items build-socket)
          (match (zmq-get-msg-parts-bytevector build-socket)
            ((worker empty rest)
             (let ((reply-worker
                    (lambda (client message)
                      (zmq-send-msg-parts-bytevector
                       build-socket
                       (list worker
                             (zmq-empty-delimiter)
                             client
                             (zmq-empty-delimiter)
                             (string->bv message))))))
               (read-worker-exp (bv->string rest)
                                #:reply-worker reply-worker)))
            ((worker empty client empty rest)
             (let ((message (list client (zmq-empty-delimiter) rest)))
               (if (need-fetching? (bv->string rest))
                   (zmq-send-msg-parts-bytevector fetch-socket message)
                   (zmq-send-msg-parts-bytevector client-socket message))))))
        ;; FETCH-WORKER -> REMOTE-SERVER.
        (when (zmq-socket-ready? items fetch-socket)
          (let ((msg (zmq-get-msg-parts-bytevector fetch-socket)))
            (zmq-send-msg-parts-bytevector client-socket msg)))

        (loop)))))

\f
;;;
;;; Entry point.
;;;

;; The PID of the publish process.
(define %publish-pid
  (make-atomic-box #f))

;; The thread running the Avahi publish service.
(define %avahi-thread
  (make-atomic-box #f))

(define (signal-handler)
  "Catch SIGINT to stop the Avahi event loop and the publish process before
exiting."
  (sigaction SIGINT
    (lambda (signum)
      (let ((publish-pid (atomic-box-ref %publish-pid))
            (avahi-thread (atomic-box-ref %avahi-thread)))
        (atomic-box-set! %stop-process? #t)

        (and publish-pid
             (begin
               (kill publish-pid SIGHUP)
               (waitpid publish-pid)))

        (and avahi-thread
             (join-thread avahi-thread))

        (exit 1)))))

(define (remote-server args)
  (signal-handler)

  (with-error-handling
    (let* ((opts (args-fold* args %options
                             (lambda (opt name arg result)
                               (leave (G_ "~A: unrecognized option~%") name))
                             (lambda (arg result)
                               (leave (G_ "~A: extraneous argument~%") arg))
                             %default-options))
           (add-to-store? (assoc-ref opts 'add-to-store?))
           (backend-port (assoc-ref opts 'backend-port))
           (publish-port (assoc-ref opts 'publish-port))
           (cache (assoc-ref opts 'cache))
           (public-key
            (read-file-sexp
             (assoc-ref opts 'public-key-file)))
           (private-key
            (read-file-sexp
             (assoc-ref opts 'private-key-file))))

      (parameterize ((%add-to-store? add-to-store?)
                     (%cache-directory cache)
                     (%public-key public-key)
                     (%private-key private-key))

        (atomic-box-set!
         %publish-pid
         (publish-server publish-port
                         #:public-key public-key
                         #:private-key private-key))

        (atomic-box-set!
         %avahi-thread
         (avahi-publish-service-thread
          service-name
          #:type remote-server-service-type
          #:port backend-port
          #:stop-loop? (lambda ()
                         (atomic-box-ref %stop-process?))
          #:txt (list (string-append "publish="
                                     (number->string publish-port)))))

        (for-each (lambda (number)
                    (start-fetch-worker
                     (string-append "fetch-worker-" (number->string number))))
                  (iota 4))

        (zmq-start-proxy backend-port)))))

debug log:

solving 6217918 ...
found 6217918 in https://yhetil.org/guix-patches/87czzso4dj.fsf@gnu.org/

applying [1/1] https://yhetil.org/guix-patches/87czzso4dj.fsf@gnu.org/
diff --git a/src/cuirass/remote-server.scm b/src/cuirass/remote-server.scm
new file mode 100644
index 0000000..6217918

Checking patch src/cuirass/remote-server.scm...
Applied patch src/cuirass/remote-server.scm cleanly.

index at:
100644 62179184fafb3485fe28054395ee8fc9a9241911	src/cuirass/remote-server.scm

(*) Git path names are given by the tree(s) the blob belongs to.
    Blobs themselves have no identifier aside from the hash of its contents.^

Code repositories for project(s) associated with this public inbox

	https://git.savannah.gnu.org/cgit/guix.git

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).