#lang racket/base (provide request-handler on-continuation-expiry rerender!) (require racket/runtime-path) (require racket/set) (require racket/match) (require racket/format) (require racket/date) (require racket/string) (require racket/port) (require (only-in racket/list filter-map)) (require (only-in racket/exn exn->string)) (require net/url) (require net/uri-codec) (require web-server/servlet) (require json) (require "gravatar.rkt") (require "bootstrap.rkt") (require "html-utils.rkt") (require "packages.rkt") (require "sessions.rkt") (require "jsonp-client.rkt") (require reloadable) (require "daemon.rkt") (require "config.rkt") (require "hash-utils.rkt") (require "static.rkt") (require "package-source.rkt") (define static-urlprefix (or (@ (config) static-urlprefix) "")) (define dynamic-urlprefix (or (@ (config) dynamic-urlprefix) "")) (define dynamic-static-urlprefix (or (@ (config) dynamic-static-urlprefix) "")) (define disable-cache? (or (@ (config) disable-cache?) #f)) (define nav-index "Packages") (define nav-search "Search") (define (navbar-header) `(a ((href "http://www.racket-lang.org/")) (img ((src ,(static-resource-url "/logo-and-text.png")) (height "60") (alt "Racket Package Index"))))) (define backend-baseurl (or (@ (config) backend-baseurl) "https://pkgd.racket-lang.org")) (define default-empty-parsed-package-source (git-source "git://github.com/" #f 'git 'git "github.com" #f "" "" "")) (define COOKIE "pltsession") (define recent-seconds (or (@ (config) recent-seconds) (* 2 24 60 60))) ;; two days (define pkg-build-baseurl (or (@ (config) pkg-build-baseurl) "http://pkg-build.racket-lang.org/")) (struct draft-package (old-name name description authors tags versions) #:prefab) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (define-values (request-handler relative-named-url) (dispatch-rules [("") main-page] [("search") search-page] [("package" (string-arg)) package-page] [("package" (string-arg) "edit") edit-package-page] [("update-my-packages") update-my-packages-page] [("update-package-ring" (string-arg) (integer-arg)) #:method "post" update-package-ring-page] [("not-found") not-found-page] [("create") edit-package-page] [("login") login-page] [("register-or-reset") register-or-reset-page] [("logout") logout-page] [("json" "search-completions") json-search-completions] [("json" "tag-search-completions") json-tag-search-completions] [("json" "formal-tags") json-formal-tags] [("pkgs-all.json") pkgs-all-json] )) (define (on-continuation-expiry request) (with-site-config (bootstrap-continuation-expiry-handler request))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (define (send/suspend/dispatch/dynamic proc) (send/suspend/dispatch (lambda (embed-url) (proc (lambda args (string-append dynamic-urlprefix (apply embed-url args))))))) (define (send/suspend/dynamic proc) (send/suspend (lambda (k-url) (proc (string-append dynamic-urlprefix k-url))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (define (named-url . args) (string-append dynamic-urlprefix (apply relative-named-url args))) (define (static-resource-url suffix) (if (rendering-static-page?) (string-append static-urlprefix suffix) (string-append dynamic-static-urlprefix suffix))) (define-syntax-rule (authentication-wrap #:request request body ...) (authentication-wrap* #f request (lambda () body ...))) (define-syntax-rule (authentication-wrap/require-login #:request request body ...) (authentication-wrap* #t request (lambda () body ...))) (define-syntax-rule (with-site-config body ...) (parameterize ((bootstrap-navbar-header (navbar-header)) (bootstrap-head-extra `((link ((rel "alternate") (type "application/atom+xml") (title "Atom Feed") (href ,(static-resource-url "/atom.xml")))))) (bootstrap-navigation `((,nav-index ,(main-page-url)) ("Documentation" "https://docs.racket-lang.org/") (,nav-search ,(named-url search-page)) ("About" (("The Racket Package System" "http://docs.racket-lang.org/pkg/getting-started.html") ("Package Builds" "https://pkg-build.racket-lang.org/about.html"))) ((div ,(glyphicon 'download-alt) " Download Racket") "http://download.racket-lang.org/") )) (bootstrap-static-urlprefix (if (rendering-static-page?) static-urlprefix dynamic-static-urlprefix)) (bootstrap-dynamic-urlprefix dynamic-urlprefix) (bootstrap-inline-js (string-append (format "PkgSiteDynamicBaseUrl = '~a';" dynamic-urlprefix) (format "PkgSiteStaticBaseUrl = '~a';" static-urlprefix) (format "IsStaticPage = ~a;" (if (rendering-static-page?) "true" "false")))) (jsonp-baseurl backend-baseurl)) body ...)) (define clear-session-cookie (make-cookie COOKIE "" #:path "/" #:expires "Thu, 01 Jan 1970 00:00:00 GMT")) (define-syntax-rule (with-session-cookie cookie-value body ...) (let ((v cookie-value)) (parameterize ((bootstrap-cookies (if v (list (make-cookie COOKIE v #:path "/" #:secure? #t)) (list clear-session-cookie)))) body ...))) (define (request->session request) (define session-cookies (filter (lambda (c) (equal? (client-cookie-name c) COOKIE)) (request-cookies request))) (define session-keys (map client-cookie-value session-cookies)) ;; (log-info "Session keys from cookie: ~a" session-keys) (for/or ((k session-keys)) (lookup-session/touch! k))) (define (authentication-wrap* require-login? request body) (define session (request->session request)) ;; (log-info "session: ~a" session) (define requested-url (url->string (request-uri request))) (if (and require-login? (not session)) (login-or-register-flow* (string-append dynamic-urlprefix requested-url) login-form) (parameterize ((bootstrap-navbar-extension (cond [(not session) `((a ((id "register-button") (class "btn btn-default navbar-btn navbar-right") (href ,(login-or-register-url requested-url (named-url register-or-reset-page)))) "Register") (a ((id "sign-in-button") (class "btn btn-success navbar-btn navbar-right") (href ,(login-or-register-url requested-url (named-url login-page)))) "Sign in"))] [else `((ul ((class "nav navbar-nav navbar-right")) (li ((class "dropdown")) (a ((class "dropdown-toggle") (data-toggle "dropdown")) (img ((src ,(gravatar-image-url (session-email session) 48)))) " " ,(session-email session) " " (span ((class "caret")))) (ul ((class "dropdown-menu") (role "menu")) (li (a ((href ,(named-url update-my-packages-page))) ,(glyphicon 'refresh) " Rescan all my packages")) (li ((class "divider"))) (li (a ((href ,(named-url edit-package-page))) ,(glyphicon 'plus-sign) " New package")) (li (a ((href ,(tags-page-url (list (format "author:~a" (session-email session)))))) ,(glyphicon 'user) " My packages")) (li ((class "divider"))) (li (a ((href ,(login-or-register-url requested-url (named-url logout-page)))) ,(glyphicon 'log-out) " Log out"))))))])) (current-session session) (bootstrap-cookies (if session (list (make-cookie COOKIE (session-key session) #:path "/" #:secure? #t)) (list)))) (with-site-config (body))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (define ((generic-input type) name [initial-value ""] #:placeholder [placeholder #f]) `(input ((class "form-control") (type ,type) (name ,name) (id ,name) ,@(maybe-splice placeholder `(placeholder ,placeholder)) (value ,initial-value)))) (define email-input (generic-input "email")) (define password-input (generic-input "password")) (define text-input (generic-input "text")) (define (label for . content) `(label ((class "control-label") ,@(maybe-splice for `(for ,for))) ,@content)) (define (primary-button . content) `(button ((type "submit") (class "btn btn-primary")) ,@content)) (define (generic-row class) (define (wrap cell) (match cell [(cons 'label _) cell] [_ `(div ,cell)])) (lambda (#:id [id #f] . args) `(div (,@(maybe-splice id `(id ,id)) (class ,class)) ,@(let loop ((args args)) (match args [(list* _ _ #f rest) (loop rest)] [(list* 0 0 cell rest) (cons cell (loop rest))] [(list* 0 w cell rest) (cons (add-classes (list (format "col-sm-~a" w)) (wrap cell)) (loop rest))] [(list* o w cell rest) (cons (add-classes (list (format "col-sm-offset-~a col-sm-~a" o w)) (wrap cell)) (loop rest))] ['() '()]))))) (define form-group (generic-row "form-group")) (define row (generic-row "row")) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (define (login-or-register-url k baseurl) (format "~a?~a" baseurl (alist->form-urlencoded (list (cons 'k (string-append dynamic-urlprefix k)))))) (define (login-or-register-flow request thunk) (define-form-bindings request ([k (named-url main-page)])) (define session (request->session request)) (if session (with-site-config (bootstrap-redirect k)) (login-or-register-flow* k thunk))) (define (login-or-register-flow* k thunk) (with-session-cookie (thunk) (with-site-config (bootstrap-redirect k)))) (define (login-page request) (login-or-register-flow request login-form)) (define (register-or-reset-page request) (login-or-register-flow request register-form)) (define (logout-page request) (define session (request->session request)) (when session (destroy-session! (session-key session))) (login-or-register-flow request (lambda () #f))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (define (login-form [error-message #f]) (with-site-config (send/suspend/dispatch/dynamic (lambda (embed-url) (bootstrap-response "Login" `(form ((class "form-horizontal") (method "post") (action ,(embed-url process-login-credentials)) (role "form")) ,(form-group 2 2 (label "email" "Email address") 0 5 (email-input "email")) ,(form-group 2 2 (label "password" "Password:") 0 5 (password-input "password")) ,(form-group 4 5 `(a ((href ,(embed-url (lambda (req) (register-form))))) "Need to reset your password?")) ,(form-group 4 5 `(a ((href ,(embed-url (lambda (req) (register-form))))) "Register an account")) ,@(maybe-splice error-message (form-group 4 5 `(div ((class "alert alert-danger")) (p ,error-message)))) ,(form-group 4 5 (primary-button "Log in")))))))) (define (authenticate-with-server! email password code) (simple-json-rpc! #:sensitive? #t #:include-credentials? #f "/api/authenticate" (hash 'email email 'passwd password 'code code))) (define (authentication-success->curator? success) (match success [#t #f] ;; new user -- we can only assume they are *not* curators [(hash-table ('curation curator?) _ ...) (if curator? #t #f)])) (define (process-login-credentials request) (define-form-bindings request (email password)) (if (or (equal? (string-trim email) "") (equal? (string-trim password) "")) (login-form "Please enter your email address and password.") (match (authenticate-with-server! email password "") [(or "wrong-code" (? eof-object?)) (login-form "Something went awry; please try again.")] [(or "emailed" #f) (summarise-code-emailing "Incorrect password, or nonexistent user." email)] [success (create-session! email password #:curator? (authentication-success->curator? success))]))) (define (register-form #:email [email ""] #:code [code ""] #:error-message [error-message #f]) (with-site-config (send/suspend/dispatch/dynamic (lambda (embed-url) (bootstrap-response "Register/Reset Account" #:title-element "" `(div ((class "registration-step-container")) (div ((class "registration-step")) (div (h1 "Step 1") (p "Get a code"))) (span ((class "registration-step-arrow")) "→") (div ((class "registration-step")) (div (h1 "Step 2") (p "Use the code")))) `(div (h1 "Need a code?") (p "Enter your email address below, and we'll send you one.") (form ((class "form-horizontal") (method "post") (action ,(embed-url notify-of-emailing)) (role "form")) ,(form-group 1 3 (label "email" "Email address") 0 5 (email-input "email_for_code")) ,(form-group 4 5 (primary-button "Email me a code")))) `(div (h1 "Got a registration or reset code?") (p "Great! Enter it below, with your chosen password, to log in.") (form ((class "form-horizontal") (method "post") (action ,(embed-url apply-account-code)) (role "form")) ,(form-group 1 3 (label "email" "Email address") 0 5 (email-input "email" email)) ,(form-group 1 3 (label "code" "Code") 0 5 (text-input "code" code)) ,(form-group 1 3 (label "password" "Password") 0 5 (password-input "password")) ,(form-group 1 3 (label "password" "Confirm password") 0 5 (password-input "confirm_password")) ,@(maybe-splice error-message (form-group 4 5 `(div ((class "alert alert-danger")) (p ,error-message)))) ,(form-group 4 5 (primary-button "Continue"))))))))) (define (apply-account-code request) (define-form-bindings request (email code password confirm_password)) (define (retry msg) (register-form #:email email #:code code #:error-message msg)) (cond [(equal? (string-trim email) "") (retry "Please enter your email address.")] [(equal? (string-trim code) "") (retry "Please enter the code you received in your email.")] [(not (equal? password confirm_password)) (retry "Please make sure the two password fields match.")] [(equal? (string-trim password) "") (retry "Please enter a password.")] [else (match (authenticate-with-server! email password code) [(? eof-object?) (retry "Something went awry. Please try again.")] ["wrong-code" (retry "The code you entered was incorrect. Please try again.")] [(or "emailed" #f) (retry "Something went awry; you have been emailed another code. Please check your email.")] [success ;; The email and password combo we have been given is good to go. ;; Set a cookie and consider ourselves logged in. (create-session! email password #:curator? (authentication-success->curator? success))])])) (define (notify-of-emailing request) (define-form-bindings request (email_for_code)) (authenticate-with-server! email_for_code "" "") ;; TODO check result? (summarise-code-emailing "Account registration/reset code emailed" email_for_code)) (define (summarise-code-emailing reason email) (with-site-config (send/suspend/dispatch/dynamic (lambda (embed-url) (bootstrap-response reason `(p "We've emailed an account registration/reset code to " (code ,email) ". Please check your email and then click " "the button to continue:") `(a ((class "btn btn-primary") (href ,(embed-url (lambda (req) (register-form))))) "Enter your code")))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (define (use-cache?) ;; We use the cache if it isn't disabled, but ONLY when the user is ;; not logged in to an account. When they are logged in, they see ;; user-specific options which don't cache well. (not (or (current-session) disable-cache?))) (define (main-page-url) (if (use-cache?) (format "~a/index.html" static-urlprefix) (named-url main-page))) (define (view-package-url package-name) (define package-name-str (~a package-name)) (if (use-cache?) (format "~a~a" static-urlprefix (relative-named-url package-page package-name-str)) (named-url package-page package-name-str))) (define (package-link package-name) `(a ((href ,(view-package-url package-name))) ,(~a package-name))) (define (doc-destruct doc) (match doc [(list _ n u) (values n u)] [(list _ n) (values n #f)])) (define (doc-link doc) (define-values (docset-name docset-url) (doc-destruct doc)) (if docset-url (buildhost-link docset-url docset-name) `(del ,docset-name))) (define (tags-page-url tags) (format "~a?~a" (named-url search-page) (alist->form-urlencoded (list (cons 'tags (string-join tags)))))) (define (author-link author-name #:gravatar? [gravatar? #f]) `(a ((href ,(tags-page-url (list (format "author:~a" author-name))))) ,@(maybe-splice gravatar? `(img ((src ,(gravatar-image-url author-name 48)))) " ") ,author-name)) (define (tag-link tag-name) `(a ((href ,(tags-page-url (list tag-name)))) ,tag-name)) (define (buildhost-link #:attributes [attributes '()] url-suffix label) `(a (,@attributes (href ,(format "~a~a" pkg-build-baseurl url-suffix))) ,label)) (define (authors-list authors #:gravatars? [gravatars? #f]) `(ul ((class "authors")) ,@(for/list ((author authors)) `(li ,(author-link author #:gravatar? gravatars?))))) (define (package-links #:pretty? [pretty? #t] package-names) (if (and pretty? (null? package-names)) `(span ((class "packages none")) "None") `(ul ((class "list-inline packages")) ,@(for/list ((p package-names)) `(li ,(package-link p)))))) (define (doc-links docs) `(ul ((class "list-inline doclinks")) ,@(for/list ((doc (or docs '()))) `(li ,(doc-link doc))))) (define (tag-links tags) `(ul ((class "list-inline taglinks")) ,@(for/list ((tag (or tags '()))) `(li ,(tag-link tag))))) (define (utc->string utc) (if (and utc (not (zero? utc))) (string-append (date->string (seconds->date utc #f) #t) " (UTC)") "N/A")) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Package hashtable getters. ;; TODO factor this stuff out into a proper data structure ;; Mandatory -- never #f (define (package-name pkg) (@ pkg name)) ;; Optional -- sometimes #f (define (package-build-failure-log pkg) (@ pkg build failure-log)) (define (package-build-success-log pkg) (@ pkg build success-log)) (define (package-build-dep-failure-log pkg) (@ pkg build dep-failure-log)) (define (package-build-conflicts-log pkg) (@ pkg build conflicts-log)) (define (package-ring pkg) (@ pkg ring)) (define (package-checksum-error pkg) (@ pkg checksum-error)) (define (package-readme-url pkg) (@ (package-external-information (string->symbol (@ pkg name))) readme-url)) (define (package-default-version pkg) (@ (package-versions pkg) default)) (define (package-locally-modified? pkg) (@ pkg _LOCALLY_MODIFIED_)) ;; If absent, default values substituted (define (package-last-updated pkg) (or (@ pkg last-updated) 0)) (define (package-last-checked pkg) (or (@ pkg last-checked) 0)) (define (package-last-edit pkg) (or (@ pkg last-edit) 0)) (define (package-authors pkg) (or (@ pkg authors) '())) (define (package-description pkg) (or (@ pkg description) "")) (define (package-tags pkg) (or (@ pkg tags) '())) (define (package-versions pkg) (or (@ pkg versions) (hash))) (define (package-docs pkg) (or (@ pkg build docs) '())) (define (package-conflicts pkg) (or (@ pkg conflicts) '())) (define (package-dependencies pkg) (or (@ pkg dependencies) '())) (define (package-modules pkg) (or (@ pkg modules) '())) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (define (package-summary-table package-names) (define now (/ (current-inexact-milliseconds) 1000)) `(table ((class "packages sortable")) (thead (tr (th 'nbsp) (th "Package") (th "Description") (th "Build"))) (tbody ,@(maybe-splice (null? package-names) `(tr (td ((colspan "4")) (div ((class "alert alert-info")) "No packages found.")))) ,@(for/list ((pkg (package-batch-detail package-names))) `(tr (td (span ((class "last-updated-negated") (style "display: none")) ,(~a (- (package-last-updated pkg)))) ,@(maybe-splice (< (- now (package-last-updated pkg)) recent-seconds) `(span ((class "label label-info")) "New"))) (td (h2 ,(package-link (package-name pkg))) ,(authors-list (package-authors pkg))) (td (p ,(package-description pkg)) ,@(maybe-splice (or (pair? (package-docs pkg)) (package-readme-url pkg)) `(div (span ((class "doctags-label")) "Docs: ") ,(doc-links (package-docs pkg)) ,@(maybe-splice (package-readme-url pkg) " " `(a ((href ,(package-readme-url pkg))) "README")) )) ,@(maybe-splice (pair? (package-tags pkg)) `(div (span ((class "doctags-label")) "Tags: ") ,(tag-links (package-tags pkg))))) ,(cond [(package-build-failure-log pkg) `(td ((class "build_red")) ,(buildhost-link (package-build-failure-log pkg) "fails"))] [(and (package-build-success-log pkg) (package-build-dep-failure-log pkg)) `(td ((class "build_yellow")) ,(buildhost-link (package-build-success-log pkg) "succeeds") " with " ,(buildhost-link (package-build-dep-failure-log pkg) "dependency problems"))] [(package-build-success-log pkg) `(td ((class "build_green")) ,(buildhost-link (package-build-success-log pkg) "succeeds"))] [else `(td)])))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (define (main-page request) (parameterize ((bootstrap-active-navigation nav-index) (bootstrap-page-scripts (list (static-resource-url "/searchbox.js") (static-resource-url "/index.js")))) (define package-name-list (package-search "" '((main-distribution #f)))) (authentication-wrap #:request request (if (and (not (rendering-static-page?)) (use-cache?)) (bootstrap-redirect (main-page-url)) (bootstrap-response "Racket Package Index" #:title-element "" #:body-class "main-page" `(div ((class "jumbotron")) (h1 "Racket Packages") (p "These are the packages in the official " (a ((href "http://docs.racket-lang.org/pkg/getting-started.html")) "package catalog") ".") (p (a ((href "http://docs.racket-lang.org/pkg/cmdline.html")) (kbd "raco pkg install " (var "package-name"))) " installs a package.") (p "You can " (a ((id "create-package-link") (href ,(named-url edit-package-page))) (span ((class "label label-success")) ,(glyphicon 'plus-sign) " add your own")) " packages to the index.")) `(div ((id "search-box")) (form ((role "form") (action ,(named-url search-page))) ,(text-input "q" #:placeholder "Search packages"))) `(div (p ((class "package-count")) ,(format "~a packages" (length package-name-list))) ,(package-summary-table package-name-list)) `(div ((class "jumbotron")) (p "Questions? Comments? Bugs? Email " (a ((href "mailto:tonyg@ccs.neu.edu")) "tonyg@ccs.neu.edu") " or " (a ((href "mailto:jay.mccarthy@gmail.com")) "jay.mccarthy@gmail.com") "."))))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (define (build-status buildhost-url str label-type glyphicon-type) `(p ((class "build-status")) "Build status: " ,(buildhost-link buildhost-url `(span ((class ,(format "label label-~a" label-type))) ,(glyphicon glyphicon-type) " " ,str)))) (define (dependencies->package-names deps) (filter-map (lambda (dep) (match dep [(? string? package-name) package-name] [(cons (? string? package-name) _) package-name] [_ (log-warning "dependencies->package-names: unknown dependency format: ~v" dep) #f])) deps)) (define (clamp-ring r) (max 0 (min 2 r))) (define (ring-change-link pkg proposed-new-ring link-content) (define new-ring (clamp-ring proposed-new-ring)) `(form ((role "form") (class "ring-change-link") (method "post") (action ,(named-url update-package-ring-page (~a (package-name pkg)) new-ring))) (button ((class "btn btn-danger btn-xs") ,@(maybe-splice (= new-ring (package-ring pkg)) `(disabled "disabled")) (type "submit")) ,link-content))) (define (not-found-page request [package-name-str #f]) (authentication-wrap #:request request (bootstrap-response #:code 404 #:message #"Page not found" "Page not found" `(div "The page you requested does not exist.") `(ul (li (a ((href ,(main-page-url))) "Return to the package index")))))) (define (package-page request package-name-str) (define package-name (string->symbol package-name-str)) (define pkg (package-detail package-name)) (authentication-wrap #:request request (cond [(not pkg) (bootstrap-response #:code 404 #:message #"No such package" "Package not found" (if package-name-str `(div "The package " (code ,package-name-str) " does not exist.") `(div "The requested package does not exist.")) `(ul (li (a ((href ,(main-page-url))) "Return to the package index"))))] [(and (not (rendering-static-page?)) (use-cache?)) (bootstrap-redirect (view-package-url package-name))] [else (let ((default-version (package-default-version pkg))) (bootstrap-response (~a package-name) #:title-element "" `(div ((class "jumbotron")) (h1 ,(~a package-name)) (p ,(package-description pkg)) ,(cond [(package-build-failure-log pkg) (build-status (package-build-failure-log pkg) "failed" "danger" "fire")] [(and (package-build-success-log pkg) (package-build-dep-failure-log pkg)) (build-status (package-build-dep-failure-log pkg) "problems" "warning" "question-sign")] [(package-build-success-log pkg) (build-status (package-build-success-log pkg) "ok" "success" "ok")] [else ""]) (div ((class "dropdown")) ,@(let ((docs (package-docs pkg))) (match docs [(list) `()] [(list doc) (define-values (n u) (doc-destruct doc)) (list (buildhost-link #:attributes `((class "btn btn-success btn-lg")) u `(span ,(glyphicon 'file) " Documentation")))] [_ `((button ((class "btn btn-success btn-lg dropdown-toggle") (data-toggle "dropdown")) ,(glyphicon 'file) " Documentation " (span ((class "caret")))) (ul ((class "dropdown-menu") (role "menu")) ,@(for/list ((doc docs)) `(li ,(doc-link doc)))))])) " " ,@(maybe-splice (package-readme-url pkg) `(a ((class "btn btn-info btn-lg") (href ,(package-readme-url pkg))) ,(glyphicon 'eye-open) " README")) ;; Heuristic guess as to whether we should present a "browse" ;; link or a "download" link. " " ,(if (equal? (@ default-version source) (@ default-version source_url)) `(a ((class "btn btn-default btn-lg") (href ,(@ default-version source_url))) ,(glyphicon 'download) " Download" ;; ,(if (regexp-match? "(?i:\\.zip$)" (or (@ default-version source_url) "")) ;; " Zip file" ;; " Download") ) `(a ((class "btn btn-default btn-lg") (href ,(package-source->human-tree-url (@ default-version source)))) ,(glyphicon 'link) " Code")) ,@(maybe-splice (member (current-email) (package-authors pkg)) " " `(a ((class "btn btn-info btn-lg") (href ,(named-url edit-package-page package-name-str))) ,(glyphicon 'edit) " Edit this package")) )) (if (package-locally-modified? pkg) `(div ((class "alert alert-warning") (role "alert")) ,(glyphicon 'exclamation-sign) " This package has been modified since the package index was last rebuilt." " The next index refresh is scheduled for " ,(utc->string (/ (next-fetch-deadline) 1000)) ".") "") (if (package-checksum-error pkg) `(div ((class "alert alert-danger") (role "alert")) (span ((class "label label-danger")) "Checksum error") " The package checksum does not match" " the package source code.") "") `(table ((class "package-details")) (tr (th "Authors") (td (div ((class "authors-detail")) ,(authors-list #:gravatars? #t (package-authors pkg))))) (tr (th "Documentation") (td ,(doc-links (package-docs pkg)))) (tr (th "Tags") (td ,(tag-links (package-tags pkg)))) (tr (th "Last updated") (td ,(utc->string (package-last-updated pkg)))) (tr (th "Ring") (td ,(~a (or (package-ring pkg) "N/A")) ,@(maybe-splice (and (package-ring pkg) (current-session) (session-curator? (current-session))) " " (ring-change-link pkg (- (package-ring pkg) 1) 'blacktriangledown) (ring-change-link pkg (+ (package-ring pkg) 1) 'blacktriangle)))) (tr (th "Conflicts") (td ,(package-links (package-conflicts pkg)))) (tr (th "Dependencies") (td ,(package-links (dependencies->package-names (package-dependencies pkg))))) (tr (th "Most recent build results") (td (ul ((class "build-results")) ,@(maybe-splice (package-build-success-log pkg) `(li "Compiled successfully: " ,(buildhost-link (package-build-success-log pkg) "transcript"))) ,@(maybe-splice (package-build-failure-log pkg) `(li "Compiled unsuccessfully: " ,(buildhost-link (package-build-failure-log pkg) "transcript"))) ,@(maybe-splice (package-build-conflicts-log pkg) `(li "Conflicts: " ,(buildhost-link (package-build-conflicts-log pkg) "details"))) ,@(maybe-splice (package-build-dep-failure-log pkg) `(li "Dependency problems: " ,(buildhost-link (package-build-dep-failure-log pkg) "details"))) ))) ,@(let* ((vs (package-versions pkg)) (empty-checksum "9f098dddde7f217879070816090c1e8e74d49432") (vs (for/hash (((k v) (in-hash vs)) #:when (not (equal? (@ v checksum) empty-checksum))) (values k v)))) (maybe-splice (not (hash-empty? vs)) `(tr (th "Versions") (td (table ((class "package-versions")) (tr (th "Version") (th "Source") (th "Checksum")) ,@(for/list (((version-sym v) (in-hash vs))) `(tr (td ,(~a version-sym)) (td (a ((href ,(package-source->human-tree-url (@ v source)))) ,(@ v source))) (td ,(@ v checksum))))))))) (tr (th "Last checked") (td ,(utc->string (package-last-checked pkg)))) (tr (th "Last edited") (td ,(utc->string (package-last-edit pkg)))) (tr (th "Modules") (td (ul ((class "module-list")) ,@(for/list ((mod (package-modules pkg))) (match-define (list kind path) mod) `(li ((class ,kind)) ,path))))) )))]))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (define (edit-package-page request [package-name-str ""]) (authentication-wrap/require-login #:request request (define package-name (string->symbol package-name-str)) (define pkg (package-detail package-name)) (cond [(and pkg (not (member (current-email) (package-authors pkg)))) ;; Not ours. Show it instead. (bootstrap-redirect (view-package-url package-name))] [(not pkg) ;; Doesn't exist. (package-form #f (draft-package "" package-name-str "" (list (current-email)) '() `(("default" ,default-empty-parsed-package-source))))] [else (package-form #f (draft-package package-name-str package-name-str (package-description pkg) (package-authors pkg) (package-tags pkg) (for/list (((ver info) (in-hash (package-versions pkg)))) (define-values (parsed complaints) (parse-package-source (@ info source))) (list (symbol->string ver) parsed))))]))) (define (package-source-option source-type value label) `(option ((value ,value) ,@(maybe-splice (equal? source-type value) '(selected "selected"))) ,label)) (define (put-default-first alist) (define default (assoc "default" alist)) (cons default (remove default alist))) (define (package-form error-message draft) (with-site-config (send/suspend/dispatch/dynamic (lambda (embed-url) (define (build-versions-table) `(table ((class "package-versions")) (tr (th "Version") (th "Source")) ,@(for/list ((v (put-default-first (draft-package-versions draft)))) (match-define (list version parsed-source) v) (define (control-name c) (format "version__~a__~a" version c)) (define (group-name c) (format "version__~a__~a__group" version c)) (define (textfield name label-text value [placeholder ""]) (row #:id (group-name name) 0 3 (and label-text (label (control-name name) label-text)) 0 (if label-text 9 12) (text-input (control-name name) value #:placeholder placeholder))) (define-values (source-type simple-url g-transport g-host+port g-repo g-commit g-path) (match parsed-source [#f (values "simple" "" "" "" "" "" "")] [(simple-url-source u _ _) (values "simple" u "" "" "" "" "")] [(git-source _ _ _ tr host port repo c path) (values "git" "" (symbol->string tr) (match* (tr port) [(_ #f) host] [(http 80) host] [(https 443) host] [(git 9418) host] [(_ _) (format "~a:~a" host port)]) repo (match c ["master" ""] [_ c]) path)])) `(tr (td ,version ,@(maybe-splice (not (equal? version "default")) " " `(button ((class "btn btn-danger btn-xs") (type "submit") (name "action") (value ,(control-name "delete"))) ,(glyphicon 'trash)))) (td ,(row 0 3 `(div ((id ,(group-name "type"))) (select ((class "package-version-source-type") (data-packageversion ,version) (name ,(control-name "type"))) ,(package-source-option source-type "git" "Git Repository") ,(package-source-option source-type "simple" "Simple URL"))) 0 9 `(div ((id ,(group-name "fields"))) (div ((id ,(group-name "urlpreview")) (class "row")) (div ((class "col-sm-3")) ,(label #f "URL preview")) (div ((class "col-sm-9")) (span ((class "form-control disabled") (disabled "disabled") (id ,(control-name "urlpreview")))))) ,(textfield "simple_url" #f simple-url) ,(textfield "g_host_port" "Host" g-host+port) ,(textfield "g_repo" "Repository" g-repo "user/repo") ,(textfield "g_commit" "Branch or commit" g-commit "master") ,(textfield "g_path" "Path within repository" g-path) ,(row #:id (group-name "g_transport") 0 3 (label (control-name "g_transport") "Transport") 0 9 `(select ((id ,(control-name "g_transport")) (name ,(control-name "g_transport"))) ,@(for/list [(t (list "git" "https" "http"))] `(option ((value ,t) ,@(maybe-splice (equal? t g-transport) '(selected "selected"))) ,t))))))))) (tr (td ((colspan "2")) (div ((class "form-inline")) ,(text-input "new_version" #:placeholder "x.y.z") " " (button ((class "btn btn-success btn-xs") (type "submit") (name "action") (value "add_version")) ,(glyphicon 'plus-sign) " Add new version")))) )) (parameterize ((bootstrap-page-scripts (list (static-resource-url "/editpackage.js")))) (define old-name (draft-package-old-name draft)) (define has-old-name? (not (equal? old-name ""))) (bootstrap-response (if has-old-name? (format "Edit package ~a" old-name) "Create a new package") #:body-class "package-form" (if error-message `(div ((class "alert alert-danger")) ,(glyphicon 'exclamation-sign) " " ,error-message) "") `(form ((id "edit-package-form") (method "post") (action ,(embed-url (update-draft draft))) (role "form")) (div ((class "container")) ;; TODO: remove?? (div ((class "row")) (div ((class "form-group col-sm-6")) ,(label "name" "Package Name") ,(text-input "name" (~a (draft-package-name draft)))) (div ((class "form-group col-sm-6")) ,(label "tags" "Package Tags (space-separated)") ,(text-input "tags" (string-join (draft-package-tags draft))))) (div ((class "row")) (div ((class "form-group col-sm-6")) ,(label "description" "Package Description") (textarea ((class "form-control") (name "description") (id "description")) ,(draft-package-description draft))) (div ((class "form-group col-sm-6")) ,(label "authors" "Author email addresses (one per line)") (textarea ((class "form-control") (name "authors") (id "authors")) ,(string-join (draft-package-authors draft) "\n")))) (div ((class "row")) (div ((class "form-group col-sm-12")) ,(label #f "Package Versions & Sources") ,(build-versions-table))) (div ((class "row")) (div ((class "form-group col-sm-12")) ,@(maybe-splice has-old-name? `(a ((class "btn btn-danger pull-right") (href ,(embed-url (confirm-package-deletion old-name)))) ,(glyphicon 'trash) " Delete package") " ") (button ((type "submit") (class "btn btn-primary") (name "action") (value "save_changes")) ,(glyphicon 'save) " Save changes") ,@(maybe-splice has-old-name? " " `(a ((class "btn btn-default") (href ,(view-package-url old-name))) "Cancel changes and return to package page")))))) )))))) (define ((confirm-package-deletion package-name-str) request) (with-site-config (send/suspend/dynamic (lambda (k-url) (bootstrap-response "Confirm Package Deletion" `(div ((class "confirm-package-deletion")) (h2 ,(format "Delete ~a?" package-name-str)) (p "This cannot be undone.") (a ((class "btn btn-default") (href ,k-url)) "Confirm deletion"))))) (jsonp-rpc! "/jsonp/package/del" `((pkg . ,package-name-str))) (define completion-ch (make-channel)) (delete-package! completion-ch (string->symbol package-name-str)) (channel-get completion-ch) (bootstrap-redirect (main-page-url)))) (define ((update-draft draft0) request) (define draft (read-draft-form draft0 (request-bindings request))) (define-form-bindings request (action new_version)) (match action ["save_changes" (if (save-draft! draft) (with-site-config (bootstrap-redirect (view-package-url (draft-package-name draft)))) (package-form "Save failed." ;; ^ TODO: This is the worst error message. ;; Right up there with "parse error". draft))] ["add_version" (cond [(equal? (string-trim new_version) "") (package-form "Please enter a version number to add." draft)] [(assoc new_version (draft-package-versions draft)) (package-form (format "Could not add version ~a, as it already exists." new_version) draft)] [else (package-form #f (struct-copy draft-package draft [versions (cons (list new_version default-empty-parsed-package-source) (draft-package-versions draft))]))])] [(regexp #px"^version__(.*)__delete$" (list _ version)) (package-form #f (struct-copy draft-package draft [versions (filter (lambda (v) (not (equal? (car v) version))) (draft-package-versions draft))]))])) (define (read-draft-form draft bindings) (define (g key d) (cond [(assq key bindings) => cdr] [else d])) (define (read-version-source version) (define (vg name d) (g (string->symbol (format "version__~a__~a" version name)) d)) (define type (vg 'type "simple")) (define simple_url (vg 'simple_url "")) (define g_transport (vg 'g_transport "")) (define g_host_port (vg 'g_host_port "")) (define g_repo0 (vg 'g_repo "")) (define g_repo (cond [(regexp-match #rx"[.]git$" g_repo0) g_repo0] [(equal? g_transport "git") g_repo0] [else (string-append g_repo0 ".git")])) (define g_commit0 (vg 'g_commit "")) (define g_path (vg 'g_path "")) (define g_commit (if (equal? g_commit0 "") "master" g_commit0)) (define-values (g_host g_port) (match (string-split g_host_port ":") [(list host) (values host #f)] [(list host (? string->number port)) (values host (string->number port))] [_ (values "" #f)])) (define source (match type ["simple" simple_url] ["git" (unparse-package-source (git-source "" #f #f (string->symbol g_transport) g_host g_port g_repo g_commit g_path))])) (define-values (parsed complaints) (parse-package-source source)) parsed) (struct-copy draft-package draft [name (g 'name (draft-package-old-name draft))] [description (g 'description "")] [authors (string-split (g 'authors ""))] [tags (string-split (g 'tags ""))] [versions (for/list ((old (draft-package-versions draft))) (match-define (list version _) old) (list version (read-version-source version)))])) (define (added-and-removed old new) (define old-set (list->set (or old '()))) (define new-set (list->set new)) (values (set->list (set-subtract new-set old-set)) (set->list (set-subtract old-set new-set)))) (define (save-draft! draft) (match-define (draft-package old-name name description authors tags versions/default) draft) (define default-version (assoc "default" versions/default)) (define source (unparse-package-source (cadr default-version))) (define versions (remove default-version versions/default)) (define old-pkg (package-detail (string->symbol old-name))) ;; name, description, and default source are updateable via /jsonp/package/modify. ;; tags are added and removed via /jsonp/package/tag/add and .../del. ;; authors are added and removed via /jsonp/package/author/add and .../del. ;; versions other than default are added and removed via /jsonp/package/version/add and .../del. ;; ;; modify-all incorporates all the add/delete stuff into a single API call. (and (or (equal? old-name name) ;; Don't let renames stomp on existing packages (not (package-detail (string->symbol name)))) (eq? #t (simple-json-rpc! "/api/package/modify-all" (hash 'pkg old-name 'name name 'description description 'source source 'tags tags 'authors authors 'versions versions))) (let* ((new-pkg (or old-pkg (hash))) (new-pkg (hash-set new-pkg 'name name)) (new-pkg (hash-set new-pkg 'description description)) (new-pkg (hash-set new-pkg 'author (string-join authors))) (new-pkg (hash-set new-pkg 'authors authors)) (new-pkg (hash-set new-pkg 'tags tags)) (new-pkg (hash-set new-pkg 'versions (friendly-versions versions/default))) (new-pkg (hash-set new-pkg 'source source)) (new-pkg (hash-set new-pkg 'search-terms (compute-search-terms new-pkg))) (new-pkg (hash-set new-pkg '_LOCALLY_MODIFIED_ #t)) (completion-ch (make-channel))) (replace-package! completion-ch old-pkg new-pkg) (channel-get completion-ch) #t))) ;; Based on (and copied from) the analogous code in meta/pkg-index/official/static.rkt (define (compute-search-terms ht) (let* ([st (hasheq)] [st (for/fold ([st st]) ([t (in-list (hash-ref ht 'tags (lambda () '())))]) (hash-set st (string->symbol t) #t))] [st (hash-set st (string->symbol (format "ring:~a" (hash-ref ht 'ring (lambda () 2)))) #t)] [st (for/fold ([st st]) ([a (in-list (string-split (hash-ref ht 'author (lambda () ""))))]) (hash-set st (string->symbol (format "author:~a" a)) #t))] [st (if (null? (hash-ref ht 'tags (lambda () '()))) (hash-set st ':no-tag: #t) st)] [st (if (hash-ref ht 'checksum-error #f) (hash-set st ':error: #t) st)] [st (if (equal? "" (hash-ref ht 'description "")) (hash-set st ':no-desc: #t) st)] [st (if (null? (hash-ref ht 'conflicts (lambda () '()))) st (hash-set st ':conflicts: #t))]) st)) (define (friendly-versions draft-versions) (for/hash ((v draft-versions)) (match-define (list version parsed) v) (values (string->symbol version) (hash 'checksum "" 'source (unparse-package-source parsed) ;; N.B. the source_url setting here survives only while we have saved it ;; locally, before the package server catches up! The package server ;; uses its own version of this code and generates its own source_url. ;; However, we ignore source_url from the package server now that ;; parsed-package-source-human-tree-url can do better. 'source_url (parsed-package-source-human-tree-url parsed))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (define (update-my-packages-page request) (authentication-wrap/require-login #:request request (jsonp-rpc! "/jsonp/update" '()) (bootstrap-response "Refresh All My Packages" `(div (p "All packages where you are listed as an author are now being rescanned.") (p "The results will be available after the next index refresh, which is " "scheduled for " ,(utc->string (/ (next-fetch-deadline) 1000)))) `(ul (li (a ((href ,(main-page-url))) "Return to the package index")))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (define (update-package-ring-page request package-name-str proposed-new-ring) (define new-ring (clamp-ring proposed-new-ring)) (authentication-wrap/require-login #:request request (when (session-curator? (current-session)) (when (jsonp-rpc! "/jsonp/package/curate" `((pkg . ,package-name-str) (ring . ,(number->string new-ring)))) (define old-pkg (package-detail (string->symbol package-name-str))) (let* ((new-pkg (hash-set old-pkg 'ring new-ring)) (completion-ch (make-channel))) (replace-package! completion-ch old-pkg new-pkg) (channel-get completion-ch)))) (bootstrap-redirect (view-package-url package-name-str)))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (define (search-page request) (parameterize ((bootstrap-active-navigation nav-search) (bootstrap-page-scripts (list (static-resource-url "/searchbox.js")))) (authentication-wrap #:request request (define-form-bindings request ([search-text q ""] [tags-input tags ""])) (define tags (for/list ((t (string-split tags-input))) (match t [(pregexp #px"!(.*)" (list _ tag)) (list (string->symbol tag) #f)] [tag (list (string->symbol tag) #t)]))) (bootstrap-response "Search Package Index" #:body-class "search-page" `(form ((class "form-horizontal") (role "form")) ,(form-group 0 2 (label "q" "Search terms") 0 10 (text-input "q" search-text #:placeholder "Enter free-form text to match here")) ,(form-group 0 2 (label "tags" "Tags") 0 10(text-input "tags" tags-input #:placeholder "tag1 tag2 tag3 ...")) ,(form-group 2 10 (primary-button (glyphicon 'search) " Search")) (div ((class "search-results")) ,@(maybe-splice (or (pair? tags) (not (equal? search-text ""))) (let ((package-name-list (package-search search-text tags))) `(div (p ((class "package-count")) ,(format "~a packages found" (length package-name-list))) ,(package-summary-table package-name-list)))))))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (define (cors-json-response f) (response/output #:mime-type #"application/json" #:headers (list (header #"Access-Control-Allow-Origin" #"*")) f)) (define (json-search-completions request) (define completions (set-union (list->set (map ~a (all-package-names))) (all-formal-tags))) (cors-json-response(lambda (response-port) (write-json (set->list completions) response-port)))) (define (json-tag-search-completions request) (cors-json-response(lambda (response-port) (write-json (set->list (all-tags)) response-port)))) (define (json-formal-tags request) (cors-json-response (lambda (response-port) (write-json (set->list (all-formal-tags)) response-port)))) (define (pkgs-all-json request) (cors-json-response (lambda (response-port) (write-json (packages-jsexpr) response-port)))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; TODO: fold the collection of this information into the package ;; database itself. (define (update-external-package-information! package-name) (define pkg (package-detail package-name)) (define default-version (package-default-version pkg)) (define external-information (and pkg (if (equal? (@ default-version source) (@ default-version source_url)) ;; We don't know where to look for a readme. (hash) ;; It's probably a github-like repo. Check for a readme. (let ((contents (with-handlers ([exn:fail:network? (lambda (e) (log-warning "Network error retrieving possible readme for ~a:\n~a" package-name (exn->string e)) "")]) (define the-port (get-pure-port (string->url (@ default-version source_url)) #:redirections 10)) (begin0 (port->string the-port) (close-input-port the-port))))) ;;(log-info "CONTENTS: ~a === ~a" (@ default-version source_url) contents) (if (regexp-match? #px"(?i:id=.readme.)" contents) (let ((readme-url (string-append (@ default-version source_url) "#readme"))) (log-info "Package ~a has a readme at ~a" package-name readme-url) (hash 'readme-url readme-url)) (hash)))))) (set-package-external-information! package-name external-information)) (define (rerender! items-to-rerender) (thread-send (package-change-handler-thread) (list 'rerender! items-to-rerender))) (define (internal:rerender-not-found!) ;; TODO: general-purpose error page instead. (static-render! #:mime-type "text/html" relative-named-url not-found-page #:ignore-response-code? #t) (log-info "Generating .htaccess") (static-put-file! "/.htaccess" (string->bytes/utf-8 (format "ErrorDocument 404 ~a~a\n" static-urlprefix (relative-named-url not-found-page))) "text/plain") (static-finish-update!)) (define (package-change-handler index-rerender-needed? pending-completions) (sync/timeout (and index-rerender-needed? (lambda () (static-render! #:mime-type "text/html" relative-named-url main-page #:filename "/index.html") (static-render! #:mime-type "application/json" relative-named-url json-search-completions) (static-finish-update!) (for ((completion-ch pending-completions)) (channel-put completion-ch (void))) (package-change-handler #f '()))) (handle-evt (thread-receive-evt) (lambda (_) (match (thread-receive) ['upgrade ;; Happens every time site.rkt is reloaded (internal:rerender-not-found!) (package-change-handler index-rerender-needed? pending-completions)] [(list 'rerender! items-to-rerender) (log-info "rerender! ~v" items-to-rerender) (define packages-to-rerender (if items-to-rerender (filter symbol? items-to-rerender) (all-package-names))) (define total-packages-to-rerender (length packages-to-rerender)) (for [(p packages-to-rerender) (i (in-naturals))] (log-info "rerendering package ~a, ~a of ~a in this batch" p (+ i 1) total-packages-to-rerender) (update-external-package-information! p) (static-render! #:mime-type "text/html" relative-named-url package-page (symbol->string p))) (package-change-handler #t pending-completions)] [(list 'package-changed completion-ch package-name) (update-external-package-information! package-name) (static-render! #:mime-type "text/html" relative-named-url package-page (symbol->string package-name)) (package-change-handler #t (if completion-ch (cons completion-ch pending-completions) pending-completions))]))))) (when (not (package-change-handler-thread)) (package-change-handler-thread (daemon-thread 'package-change-handler (lambda () (package-change-handler #f '()))))) (thread-send (package-change-handler-thread) 'upgrade) ;; switch to new code