Initial commit

This commit is contained in:
Michael Stapelberg 2017-03-04 11:22:48 +01:00
commit 38af7fd18d
21 changed files with 1949 additions and 0 deletions

27
LICENSE Normal file
View File

@ -0,0 +1,27 @@
Copyright (c) 2017 the gokrazy authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of gokrazy nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

121
README.md Normal file
View File

@ -0,0 +1,121 @@
# Overview
gokrazy packs your Go application(s) into an SD card image for the
Raspberry Pi 3 which — aside from the Linux kernel and proprietary
Raspberry Pi bootloader — only contains Go software.
The motivation is that [@stapelberg](https://github.com/stapelberg)
spends way more time on C software and their various issues than he
would like. Hence, he is going Go-only where feasible.
# Usage
## Installation
Install Go if you havent already:
```
sudo apt install golang-go
export GOPATH=~/go
export PATH=$GOPATH/bin:$PATH
```
Then, use the `go` tool to download and install `gokr-packer`:
```
go get -u github.com/gokrazy/tools/cmd/gokr-packer
```
## Overwriting an SD card for the Raspberry Pi 3
To re-partition and overwrite the SD card `/dev/sdb`, use:
```
sudo setcap CAP_SYS_ADMIN,CAP_DAC_OVERRIDE=ep ~/go/bin/gokr-packer
gokr-packer -overwrite=/dev/sdb github.com/gokrazy/hello
```
Then, put the SD card into your Raspberry Pi 3 and power it up! Once
the Raspberry Pi 3 has booted (takes about 10 seconds), you should be
able to reach the gokrazy web interface at the URL which `gokr-packer`
printed.
Under the hood, `gokr-packer`
1. …packed the latest [firmware](https://github.com/gokrazy/firmware)
and [kernel](https://github.com/gokrazy/kernel) binaries into the
boot file system.
2. …built the specified Go packages using `go install` and packed all
their binaries into the `/user` directory of the root file system.
3. …created a minimal gokrazy init program which supervises all
binaries (i.e. restarts them when they exit).
## Updating your installation
To update gokrazy, including the firmware and kernel binaries, use:
```
go get -u github.com/gokrazy/tools/cmd/gokr-packer
```
To update your gokrazy installation (running on a Raspberry Pi 3),
use:
```
gokr-packer -update=http://gokrazy:mysecretpassword@gokrazy/ github.com/gokrazy/hello
```
# SD card contents
gokrazy uses the following partition table:
num | size | purpose | file system
----|--------|------------------------|---------------
1 | 100 MB | boot (kernel+firmware) | FAT16B
2 | 500 MB | root2 (gokrazy+apps) | FAT16B (but see [issue #10](https://github.com/gokrazy/gokrazy/issues/10))
3 | 500 MB | root3 (gokrazy+apps) | FAT16B (but see [issue #10](https://github.com/gokrazy/gokrazy/issues/10))
4 | rest | permanent data | ext4
The two root partitions are used alternatingly (to avoid modifying the
currently active file system) when updating.
If youd like to store permanent data (i.e. data which will not be
overwritten on the next update), youll need to create an ext4 file
system on the last partition. If your SD card is `/dev/sdb`, use
`mkfs.ext4 /dev/sdb4`.
# Customization
## Changing program behavior for gokrazy
`gokr-packer` sets the “gokrazy” [build
tag](https://golang.org/pkg/go/build/#hdr-Build_Constraints) for
conditional compilation.
You can find an example commit which implements a gokrazy-specific
controller that triggers the main program logic every weekday at 10:00
at https://github.com/stapelberg/zkj-nas-tools/commit/6f90ace35981f78dcd66d611269f17f37ce4b4ef
## Changing init behavior
```
gokr-packer \
-overwrite_init=$(go env GOPATH)/src/github.com/stapelberg/mediaserver/cmd/init/init.go \
github.com/gokrazy/hello
```
(Note that the package must result in a binary called “init”.)
Then, edit the `github.com/stapelberg/mediaserver` package to your
liking. When done, pack an image with your own init package:
```
gokr-packer \
-init_pkg=github.com/stapelberg/mediaserver \
-overwrite=/dev/sdb \
github.com/gokrazy/hello
```
# Repository structure
* [github.com/gokrazy/gokrazy](https://github.com/gokrazy/gokrazy): system code, main issue tracker, documentation
* [github.com/gokrazy/tools](https://github.com/gokrazy/tools): SD card image creation code, pulling in:
* [github.com/gokrazy/firmware](https://github.com/gokrazy/firmware): Raspberry Pi 3 firmware files
* [github.com/gokrazy/kernel](https://github.com/gokrazy/kernel): pre-built kernel image and bootloader config

7
assets/footer.tmpl Normal file
View File

@ -0,0 +1,7 @@
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js" integrity="sha256-hVVnYaiADRTO2PzUGmuLJr8BLUSjGIZsDYGmIJLv2b8=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-table/1.11.0/bootstrap-table.min.js" integrity="sha256-eXHLyyVI+v6X1wbfg9NB05IWqOqY4E9185nHZgeDIhg=" crossorigin="anonymous"></script>
</html>

57
assets/header.tmpl Normal file
View File

@ -0,0 +1,57 @@
<!DOCTYPE html>
<html lang="en">
<title>gokrazy</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha256-916EbMg70RQy9LHiGkXzG8hSg9EdNy97GazNG/aiY1w=" crossorigin="anonymous" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-table/1.11.0/bootstrap-table.min.css" integrity="sha256-eU4xmpfQx1HSi5q1q2rHNcMEzTNJov7r2Wr/6zF3ANc=" crossorigin="anonymous" />
<style type="text/css">
.progress-bar:nth-child(5n) {
background-color: #337ab7;
}
.progress-bar:nth-child(5n+1) {
background-color: #5cb85c;
}
.progress-bar:nth-child(5n+2) {
background-color: #5bc0de;
}
.progress-bar:nth-child(5n+3) {
background-color: #f0ad4e;
}
.progress-bar:nth-child(5n+4) {
background-color: #d9534f;
}
.lastlog {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
table {
table-layout: fixed;
}
</style>
<nav class="navbar navbar-default">
<div class="container-fluid">
<!-- Brand and toggle get grouped for better mobile display -->
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar-collapse-1" aria-expanded="false">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<p style="width: 50ex; margin-top: 0.25em; font-size: 18px"><a href="/">gokrazy</a><br>
<small style="font-size: 11px" class="text-muted">version {{ .BuildTimestamp }}</small></p>
</div>
<div class="collapse navbar-collapse" id="navbar-collapse-1">
<ul class="nav navbar-nav">
</ul>
<p class="navbar-text navbar-right">
host “{{ .Hostname }}”
</p>
</div><!-- /.navbar-collapse -->
</div><!-- /.container-fluid -->
</nav>
<div class="container">

82
assets/overview.tmpl Normal file
View File

@ -0,0 +1,82 @@
{{ template "header" . }}
<div class="row">
<div class="col-md-12">
<h1>services</h1>
<table class="table">
<tbody><tr>
<th width="20%">path</th>
<th width="80%">last log line</th>
</tr>
{{ range $idx, $svc := .Services }}
<tr>
<td>
<a href="/status?path={{ $svc.Name }}">{{ $svc.Name }}</a>
{{ if restarting $svc.Started }}
<span class="label label-danger">restarting</span>
{{ end }}
{{ if $svc.Stopped }}
<span class="label label-warning">stopped</span>
{{ end }}
</td>
<td class="lastlog">
{{ last $svc.Stdout.Lines $svc.Stderr.Lines }}
</td>
</tr>
{{ end }}
</table>
</div>
<div class="col-md-12">
<h2>memory</h2>
{{ megabytes (index .Meminfo "MemTotal") }} total, {{ megabytes (index .Meminfo "MemAvailable") }} available<br>
<strong>resident set size (RSS) by service</strong>:
<div class="progress">
{{ with $rss := initRss }}
<div class="progress-bar" style="width: {{ rssPercentage $.Meminfo $rss }}%" title="init uses {{ megabytes $rss }} RSS">
<span class="sr-only"></span>
init
</div>
{{ end }}
{{ range $idx, $svc := .Services }}
{{ with $rss := $svc.RSS }}
<div class="progress-bar" style="width: {{ rssPercentage $.Meminfo $rss }}%" title="{{ $svc.Name }} uses {{ megabytes $rss }} RSS">
<span class="sr-only"></span>
{{ baseName $svc.Name }}
</div>
{{ end }}
{{ end }}
<div class="progress-bar" style="width: 100%; overflow:initial; float: none" title="memory usage outside of gokrazy services">
<span class="sr-only"></span>
unaccounted
</div>
</div>
</div>
<div class="col-md-12">
<h2>storage</h2>
{{ if eq .PermAvail 0 }}
No permanent storage mounted. To create a filesystem for permanent storage, plug the SD card into a Linux computer and, if your SD card is <code>/dev/sdb</code>, use <code>mkfs.ext4 /dev/sdb4</code>.
{{ else }}
<strong>/dev/mmcblk0p4</strong>: {{ gigabytes .PermTotal }} total, {{ gigabytes .PermUsed }} used, {{ gigabytes .PermAvail }} avail<br>
{{ end }}
<h2>private network addresses</h2>
<ul>
{{ range $idx, $host := .Hosts }}
<li>{{ $host }}</li>
{{ end }}
</ul>
</div>
</div>
{{ template "footer" . }}

34
assets/status.tmpl Normal file
View File

@ -0,0 +1,34 @@
{{ template "header" . }}
<div class="row">
<div class="col-md-12">
<table>
<tr>
<th>Name</th>
<th>Started</th>
<th>Actions</th>
</tr>
<tr>
<td><a href="#{{ .Service.Name }}">{{ .Service.Name }}</a></td>
<td>{{ .Service.Started }}</td>
<td><form method="POST" action="/restart"><input type="hidden" name="path" value="{{ .Service.Name }}"><input type="submit" value="restart"></form><form method="POST" action="/stop"><input type="hidden" name="path" value="{{ .Service.Name }}"><input type="submit" value="stop"></form></td>
</tr>
</table>
<h3>stdout</h3>
<pre>
{{ range $idx, $line := .Service.Stdout.Lines -}}
{{ $line }}
{{ end }}
</pre>
<h3>stderr</h3>
<pre>
{{ range $idx, $line := .Service.Stderr.Lines -}}
{{ $line }}
{{ end }}
</pre>
</div>
</div>
{{ template "footer" . }}

38
authenticated.go Normal file
View File

@ -0,0 +1,38 @@
package gokrazy
import (
"encoding/base64"
"fmt"
"net/http"
"strings"
)
func authenticated(w http.ResponseWriter, r *http.Request) {
// defense in depth
if httpPassword == "" {
http.Error(w, "httpPassword not set", http.StatusInternalServerError)
return
}
s := strings.SplitN(r.Header.Get("Authorization"), " ", 2)
if len(s) != 2 || s[0] != "Basic" {
w.Header().Set("WWW-Authenticate", `Basic realm="gokrazy"`)
http.Error(w, "no Basic Authorization header set", http.StatusUnauthorized)
return
}
b, err := base64.StdEncoding.DecodeString(s[1])
if err != nil {
http.Error(w, fmt.Sprintf("could not decode Authorization header as base64: %v", err), http.StatusUnauthorized)
return
}
pair := strings.SplitN(string(b), ":", 2)
if len(pair) != 2 ||
pair[0] != "gokrazy" ||
pair[1] != httpPassword {
http.Error(w, "invalid username/password", http.StatusUnauthorized)
return
}
http.DefaultServeMux.ServeHTTP(w, r)
}

3
bundle.go Normal file
View File

@ -0,0 +1,3 @@
package gokrazy
//go:generate sh -c "go run goembed.go -package bundled -var assets assets/header.tmpl assets/footer.tmpl assets/overview.tmpl assets/status.tmpl > internal/bundled/GENERATED_bundled.go"

233
cmd/dhcp/dhcp.go Normal file
View File

@ -0,0 +1,233 @@
// Only build for (linux AND (amd64 OR arm64)) due to using
// linux-specific syscalls with uint64 for “unsigned long”:
// +build linux
// +build amd64 arm64
// dhcp is a minimal DHCP client for gokrazy.
package main
import (
"encoding/binary"
"fmt"
"io/ioutil"
"log"
"net"
"os"
"strings"
"syscall"
"time"
"golang.org/x/sys/unix"
"github.com/d2g/dhcp4"
"github.com/d2g/dhcp4client"
"github.com/gokrazy/gokrazy/internal/iface"
)
func parseDHCPDuration(b []byte) time.Duration {
return time.Duration(binary.BigEndian.Uint32(b)) * time.Second
}
var (
defaultDst = net.IP([]byte{0, 0, 0, 0})
defaultNetmask = net.IPMask([]byte{0, 0, 0, 0})
)
// dhcpRequest is a copy of (dhcp4client/Client).Request which
// includes the hostname.
func dhcpRequest(c *dhcp4client.Client) (bool, dhcp4.Packet, error) {
var utsname unix.Utsname
if err := unix.Uname(&utsname); err != nil {
log.Fatal(err)
}
nnb := make([]byte, 0, len(utsname.Nodename))
for _, i := range utsname.Nodename {
if i == 0 {
break
}
nnb = append(nnb, byte(i))
}
discoveryPacket := c.DiscoverPacket()
discoveryPacket.AddOption(dhcp4.OptionHostName, nnb)
discoveryPacket.PadToMinSize()
if err := c.SendPacket(discoveryPacket); err != nil {
return false, discoveryPacket, err
}
offerPacket, err := c.GetOffer(&discoveryPacket)
if err != nil {
return false, offerPacket, err
}
requestPacket := c.RequestPacket(&offerPacket)
requestPacket.AddOption(dhcp4.OptionHostName, nnb)
requestPacket.PadToMinSize()
if err := c.SendPacket(requestPacket); err != nil {
return false, requestPacket, err
}
acknowledgement, err := c.GetAcknowledgement(&requestPacket)
if err != nil {
return false, acknowledgement, err
}
acknowledgementOptions := acknowledgement.ParseOptions()
if dhcp4.MessageType(acknowledgementOptions[dhcp4.OptionDHCPMessageType][0]) != dhcp4.ACK {
return false, acknowledgement, nil
}
return true, acknowledgement, nil
}
func main() {
log.SetFlags(log.LstdFlags | log.Lshortfile)
eth0, err := net.InterfaceByName("eth0")
if err != nil {
log.Fatal(err)
}
cs, err := iface.NewConfigSocket("eth0")
if err != nil {
log.Fatalf("config socket: %v", err)
}
defer cs.Close()
// Ensure the interface is up so that we can send DHCP packets.
if err := cs.Up(); err != nil {
log.Fatal(err)
}
// Wait for up to 10 seconds for the link to indicate it has a
// carrier.
for i := 0; i < 10; i++ {
b, err := ioutil.ReadFile("/sys/class/net/eth0/carrier")
if err == nil && strings.TrimSpace(string(b)) == "1" {
break
}
time.Sleep(1 * time.Second)
}
pktsock, err := dhcp4client.NewPacketSock(eth0.Index)
if err != nil {
log.Fatal(err)
}
dhcp, err := dhcp4client.New(
dhcp4client.HardwareAddr(eth0.HardwareAddr),
dhcp4client.Timeout(5*time.Second),
dhcp4client.Broadcast(false),
dhcp4client.Connection(pktsock),
)
if err != nil {
log.Fatal(err)
}
ok, ack, err := dhcpRequest(dhcp)
for {
if err != nil {
log.Fatal(err)
}
if !ok {
log.Fatal("received DHCPNAK")
}
opts := ack.ParseOptions()
// DHCPACK (described in RFC2131 4.3.1)
// - yiaddr: IP address assigned to client
details := []string{
fmt.Sprintf("IP %v", ack.YIAddr()),
}
if b, ok := opts[dhcp4.OptionSubnetMask]; ok {
ipnet := net.IPNet{
IP: ack.YIAddr(),
Mask: net.IPMask(b),
}
details[0] = fmt.Sprintf("IP %v", ipnet.String())
}
if b, ok := opts[dhcp4.OptionBroadcastAddress]; ok {
details = append(details, fmt.Sprintf("broadcast %v", net.IP(b)))
}
if b, ok := opts[dhcp4.OptionRouter]; ok {
details = append(details, fmt.Sprintf("router %v", net.IP(b)))
}
if b, ok := opts[dhcp4.OptionDomainNameServer]; ok {
details = append(details, fmt.Sprintf("DNS %v", net.IP(b)))
}
log.Printf("DHCPACK: %v", strings.Join(details, ", "))
if err := cs.SetAddress(ack.YIAddr()); err != nil {
log.Fatal(err)
}
if b, ok := opts[dhcp4.OptionSubnetMask]; ok {
if err := cs.SetNetmask(net.IPMask(b)); err != nil {
log.Fatalf("setNetmask(%v): %v", net.IPMask(b), err)
}
}
if b, ok := opts[dhcp4.OptionBroadcastAddress]; ok {
if err := cs.SetBroadcast(net.IP(b)); err != nil {
log.Fatalf("setBroadcast(%v): %v", net.IP(b), err)
}
}
if err := cs.Up(); err != nil {
log.Fatal(err)
}
if b, ok := opts[dhcp4.OptionRouter]; ok {
if errno := cs.AddRoute(defaultDst, net.IP(b), defaultNetmask); errno != 0 {
if errno == syscall.EEXIST {
if errno := cs.DelRoute(defaultDst, net.IP(b), defaultNetmask); errno != 0 {
log.Printf("delRoute(%v): %v", net.IP(b), errno)
}
if errno := cs.AddRoute(defaultDst, net.IP(b), defaultNetmask); errno != 0 {
log.Fatalf("addRoute(%v): %v", net.IP(b), errno)
}
} else {
log.Fatalf("addRoute(%v): %v", net.IP(b), errno)
}
}
}
if b, ok := opts[dhcp4.OptionDomainNameServer]; ok {
// Get the symlink out of the way, if any.
if err := os.Remove("/etc/resolv.conf"); err != nil && !os.IsNotExist(err) {
log.Fatalf("resolv.conf: %v", err)
}
if err := ioutil.WriteFile("/etc/resolv.conf", []byte(fmt.Sprintf("nameserver %v\n", net.IP(b))), 0644); err != nil {
log.Fatalf("resolv.conf: %v", err)
}
}
// Notify init of new addresses
p, _ := os.FindProcess(1)
if err := p.Signal(syscall.SIGHUP); err != nil {
log.Printf("send SIGHUP to init: %v", err)
}
leaseTime := 10 * time.Minute // seems sensible as a fallback
if b, ok := opts[dhcp4.OptionIPAddressLeaseTime]; ok && len(b) == 4 {
leaseTime = parseDHCPDuration(b)
}
// As per RFC 2131 section 4.4.5:
// renewal time defaults to 50% of the lease time
renewalTime := time.Duration(float64(leaseTime) * 0.5)
if b, ok := opts[dhcp4.OptionRenewalTimeValue]; ok && len(b) == 4 {
renewalTime = parseDHCPDuration(b)
}
time.Sleep(renewalTime)
ok, ack, err = dhcp.Renew(ack)
}
}

32
cmd/ntp/ntp.go Normal file
View File

@ -0,0 +1,32 @@
// ntp is a minimal NTP client for gokrazy.
package main
import (
"log"
"math/rand"
"syscall"
"time"
"github.com/beevik/ntp"
)
func set() error {
r, err := ntp.Query("pool.ntp.org", 4)
if err != nil {
return err
}
tv := syscall.NsecToTimeval(r.Time.UnixNano())
return syscall.Settimeofday(&tv)
}
func main() {
log.SetFlags(log.LstdFlags | log.Lshortfile)
for {
if err := set(); err != nil {
log.Fatalf("setting time failed: %v", err)
}
time.Sleep(1*time.Hour + time.Duration(rand.Int63n(250))*time.Millisecond)
}
}

4
doc.go Normal file
View File

@ -0,0 +1,4 @@
// gokrazy packs your Go application(s) into an SD card image for the
// Raspberry Pi 3 which — aside from the Linux kernel and proprietary
// Raspberry Pi bootloader — only contains Go software.
package gokrazy

190
goembed.go Normal file
View File

@ -0,0 +1,190 @@
// copied from https://github.com/dsymonds/goembed/ with pull requests applied
// +build ignore
// goembed generates a Go source file from an input file.
package main
import (
"bufio"
"bytes"
"compress/gzip"
"flag"
"fmt"
"io"
"log"
"os"
"text/template"
"unicode/utf8"
)
var (
packageFlag = flag.String("package", "", "Go package name")
varFlag = flag.String("var", "", "Go var name")
gzipFlag = flag.Bool("gzip", false, "Whether to gzip contents")
)
func main() {
flag.Parse()
fmt.Printf("package %s\n\n", *packageFlag)
if *gzipFlag {
err := gzipPrologue.Execute(os.Stdout, map[string]interface{}{
"Args": flag.Args(),
"VarName": *varFlag,
})
if err != nil {
log.Fatal(err)
}
}
if flag.NArg() > 0 {
fmt.Println("// Table of contents")
fmt.Printf("var %v = map[string][]byte{\n", *varFlag)
for i, filename := range flag.Args() {
fmt.Printf("\t%#v: %s_%d,\n", filename, *varFlag, i)
}
fmt.Println("}")
// Using a separate variable for each []byte, instead of
// combining them into a single map literal, enables a storage
// optimization: the compiler places the data directly in the
// program's noptrdata section instead of the heap.
for i, filename := range flag.Args() {
if err := oneVar(fmt.Sprintf("%s_%d", *varFlag, i), filename); err != nil {
log.Fatal(err)
}
}
} else {
if err := oneVarReader(*varFlag, os.Stdin); err != nil {
log.Fatal(err)
}
}
}
func oneVar(varName, filename string) error {
f, err := os.Open(filename)
if err != nil {
return err
}
defer f.Close()
return oneVarReader(varName, f)
}
func oneVarReader(varName string, r io.Reader) error {
// Generate []byte(<big string constant>) instead of []byte{<list of byte values>}.
// The latter causes a memory explosion in the compiler (60 MB of input chews over 9 GB RAM).
// Doing a string conversion avoids some of that, but incurs a slight startup cost.
if !*gzipFlag {
fmt.Printf(`var %s = []byte("`, varName)
} else {
var buf bytes.Buffer
gzw, _ := gzip.NewWriterLevel(&buf, gzip.BestCompression)
if _, err := io.Copy(gzw, r); err != nil {
return err
}
if err := gzw.Close(); err != nil {
return err
}
fmt.Printf("var %s []byte // set in init\n\n", varName)
fmt.Printf(`var %s_gzip = []byte("`, varName)
r = &buf
}
bufw := bufio.NewWriter(os.Stdout)
if _, err := io.Copy(&writer{w: bufw}, r); err != nil {
return err
}
if err := bufw.Flush(); err != nil {
return err
}
fmt.Println(`")`)
return nil
}
type writer struct {
w io.Writer
}
func (w *writer) Write(data []byte) (n int, err error) {
n = len(data)
for err == nil && len(data) > 0 {
// https://golang.org/ref/spec#String_literals: "Within the quotes, any
// character may appear except newline and unescaped double quote. The
// text between the quotes forms the value of the literal, with backslash
// escapes interpreted as they are in rune literals […]."
switch b := data[0]; b {
case '\\':
_, err = w.w.Write([]byte(`\\`))
case '"':
_, err = w.w.Write([]byte(`\"`))
case '\n':
_, err = w.w.Write([]byte(`\n`))
case '\x00':
// https://golang.org/ref/spec#Source_code_representation: "Implementation
// restriction: For compatibility with other tools, a compiler may
// disallow the NUL character (U+0000) in the source text."
_, err = w.w.Write([]byte(`\x00`))
default:
// https://golang.org/ref/spec#Source_code_representation: "Implementation
// restriction: […] A byte order mark may be disallowed anywhere else in
// the source."
const byteOrderMark = '\uFEFF'
if r, size := utf8.DecodeRune(data); r != utf8.RuneError && r != byteOrderMark {
_, err = w.w.Write(data[:size])
data = data[size:]
continue
}
_, err = fmt.Fprintf(w.w, `\x%02x`, b)
}
data = data[1:]
}
return n - len(data), err
}
var gzipPrologue = template.Must(template.New("").Parse(`
import (
"bytes"
"compress/gzip"
"io/ioutil"
)
func init() {
var (
r *gzip.Reader
err error
)
{{ if gt (len .Args) 0 }}
{{ range $idx, $var := .Args }}
{{ $n := printf "%s_%d" $.VarName $idx }}
r, err = gzip.NewReader(bytes.NewReader({{ $n }}_gzip))
if err != nil {
panic(err)
}
{{ $n }}, err = ioutil.ReadAll(r)
r.Close()
if err != nil {
panic(err)
}
{{ end }}
{{ else }}
r, err = gzip.NewReader(bytes.NewReader({{ .VarName }}_gzip))
if err != nil {
panic(err)
}
{{ .VarName }}, err = ioutil.ReadAll(r)
r.Close()
if err != nil {
panic(err)
}
{{ end }}
}
`))

159
gokrazy.go Normal file
View File

@ -0,0 +1,159 @@
// Boot and Supervise are called by the auto-generated init
// program. They are provided in case you need to implement a custom
// init program.
//
// PrivateInterfaceAddrs is useful for init and other processes.
//
// DontStartOnBoot and WaitForClock are useful for non-init processes.
package gokrazy
import (
"fmt"
"io/ioutil"
"log"
"net"
"os"
"os/exec"
"os/signal"
"strings"
"time"
"github.com/gokrazy/gokrazy/internal/iface"
"golang.org/x/sys/unix"
)
var (
buildTimestamp = "uninitialized"
httpPassword string
hostname string
)
func configureLoopback() error {
cs, err := iface.NewConfigSocket("lo")
if err != nil {
return fmt.Errorf("config socket: %v", err)
}
defer cs.Close()
if err := cs.Up(); err != nil {
return err
}
if err := cs.SetAddress(net.IP([]byte{127, 0, 0, 1})); err != nil {
return err
}
return cs.SetNetmask(net.IPMask([]byte{255, 0, 0, 0}))
}
// Boot configures basic system settings. More specifically, it:
//
// - mounts /dev, /tmp, /proc, /sys and /perm file systems
// - mounts and populate /etc tmpfs overlay
// - sets hostname from the /hostname file
// - sets HTTP password from the gokr-pw.txt file
// - configures the loopback network interface
//
// Boot should always be called to transition the machine into a
// useful state, even in custom init process implementations or
// single-process applications.
//
// userBuildTimestamp will be exposed on the HTTP status handlers that
// are set up by Supervise.
func Boot(userBuildTimestamp string) error {
buildTimestamp = userBuildTimestamp
if err := mountfs(); err != nil {
return err
}
hostnameb, err := ioutil.ReadFile("/hostname")
if err != nil {
return err
}
if err := unix.Sethostname(hostnameb); err != nil {
return err
}
hostname = string(hostnameb)
pw, err := ioutil.ReadFile("/perm/gokr-pw.txt")
if err != nil {
pw, err = ioutil.ReadFile("/gokr-pw.txt")
if err != nil {
return fmt.Errorf("could read neither /perm/gokr-pw.txt nor /gokr-pw.txt: %v", err)
}
}
httpPassword = strings.TrimSpace(string(pw))
if err := configureLoopback(); err != nil {
return err
}
return nil
}
// Supervise continuously restarts the processes specified in commands
// unless they run DontStartOnBoot.
//
// Password-protected HTTP handlers are installed, allowing to inspect
// the supervised services and update the gokrazy installation over
// the network.
//
// HTTP is served on PrivateInterfaceAddrs(). New IP addresses will be
// picked up upon receiving SIGHUP.
func Supervise(commands []*exec.Cmd) error {
services := make([]*service, len(commands))
for idx, cmd := range commands {
services[idx] = &service{cmd: cmd}
}
superviseServices(services)
initStatus(services)
if err := initUpdate(); err != nil {
return err
}
if err := updateListeners("80"); err != nil {
return fmt.Errorf("updating listeners: %v", err)
}
go func() {
c := make(chan os.Signal, 1)
signal.Notify(c, unix.SIGHUP)
for range c {
if err := updateListeners("80"); err != nil {
log.Printf("updating listeners: %v", err)
}
}
}()
return nil
}
// WaitForClock returns once the system clock appears to have been
// set. Assumes that the system boots with a clock value of January 1,
// 1970 UTC (UNIX epoch), as is the case on the Raspberry Pi 3.
func WaitForClock() {
epochPlus1Minute := time.Unix(60, 0)
for {
if time.Now().After(epochPlus1Minute) {
return
}
// Sleeps for 1 real minute, regardless of wall-clock time.
// See https://github.com/golang/proposal/blob/master/design/12914-monotonic.md
time.Sleep(1 * time.Second)
}
}
// DontStartOnBoot informs the gokrazy init process to not supervise
// the process and exits. The user can manually start supervision,
// which turns DontStartOnBoot into a no-op.
func DontStartOnBoot() {
if os.Getenv("GOKRAZY_FIRST_START") == "1" {
os.Exit(125)
}
}

View File

@ -0,0 +1,13 @@
package bundled
// Table of contents
var assets = map[string][]byte{
"assets/header.tmpl": assets_0,
"assets/footer.tmpl": assets_1,
"assets/overview.tmpl": assets_2,
"assets/status.tmpl": assets_3,
}
var assets_0 = []byte("<!DOCTYPE html>\n<html lang=\"en\">\n<title>gokrazy</title>\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css\" integrity=\"sha256-916EbMg70RQy9LHiGkXzG8hSg9EdNy97GazNG/aiY1w=\" crossorigin=\"anonymous\" />\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/bootstrap-table/1.11.0/bootstrap-table.min.css\" integrity=\"sha256-eU4xmpfQx1HSi5q1q2rHNcMEzTNJov7r2Wr/6zF3ANc=\" crossorigin=\"anonymous\" />\n<style type=\"text/css\">\n.progress-bar:nth-child(5n) {\n background-color: #337ab7;\n}\n.progress-bar:nth-child(5n+1) {\n background-color: #5cb85c;\n}\n.progress-bar:nth-child(5n+2) {\n background-color: #5bc0de;\n}\n.progress-bar:nth-child(5n+3) {\n background-color: #f0ad4e;\n}\n.progress-bar:nth-child(5n+4) {\n background-color: #d9534f;\n}\n.lastlog {\n text-overflow: ellipsis;\n white-space: nowrap;\n overflow: hidden;\n}\ntable {\n table-layout: fixed;\n}\n</style>\n\n <nav class=\"navbar navbar-default\">\n <div class=\"container-fluid\">\n <!-- Brand and toggle get grouped for better mobile display -->\n <div class=\"navbar-header\">\n <button type=\"button\" class=\"navbar-toggle collapsed\" data-toggle=\"collapse\" data-target=\"#navbar-collapse-1\" aria-expanded=\"false\">\n <span class=\"sr-only\">Toggle navigation</span>\n <span class=\"icon-bar\"></span>\n <span class=\"icon-bar\"></span>\n <span class=\"icon-bar\"></span>\n </button>\n <p style=\"width: 50ex; margin-top: 0.25em; font-size: 18px\"><a href=\"/\">gokrazy</a><br>\n <small style=\"font-size: 11px\" class=\"text-muted\">version {{ .BuildTimestamp }}</small></p>\n </div>\n\n <div class=\"collapse navbar-collapse\" id=\"navbar-collapse-1\">\n <ul class=\"nav navbar-nav\">\n </ul>\n\n <p class=\"navbar-text navbar-right\">\n host “{{ .Hostname }}”\n </p>\n </div><!-- /.navbar-collapse -->\n </div><!-- /.container-fluid -->\n </nav>\n\n <div class=\"container\">\n")
var assets_1 = []byte("\n</div>\n\n<script src=\"https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js\" integrity=\"sha256-hVVnYaiADRTO2PzUGmuLJr8BLUSjGIZsDYGmIJLv2b8=\" crossorigin=\"anonymous\"></script>\n<script src=\"https://cdnjs.cloudflare.com/ajax/libs/bootstrap-table/1.11.0/bootstrap-table.min.js\" integrity=\"sha256-eXHLyyVI+v6X1wbfg9NB05IWqOqY4E9185nHZgeDIhg=\" crossorigin=\"anonymous\"></script>\n\n</html>")
var assets_2 = []byte("{{ template \"header\" . }}\n\n<div class=\"row\">\n<div class=\"col-md-12\">\n\n<h1>services</h1>\n\n<table class=\"table\">\n<tbody><tr>\n<th width=\"20%\">path</th>\n<th width=\"80%\">last log line</th>\n</tr>\n\n{{ range $idx, $svc := .Services }}\n<tr>\n<td>\n<a href=\"/status?path={{ $svc.Name }}\">{{ $svc.Name }}</a>\n{{ if restarting $svc.Started }}\n<span class=\"label label-danger\">restarting</span>\n{{ end }}\n{{ if $svc.Stopped }}\n<span class=\"label label-warning\">stopped</span>\n{{ end }}\n</td>\n<td class=\"lastlog\">\n{{ last $svc.Stdout.Lines $svc.Stderr.Lines }}\n</td>\n</tr>\n{{ end }}\n\n</table>\n</div> \n<div class=\"col-md-12\">\n<h2>memory</h2>\n{{ megabytes (index .Meminfo \"MemTotal\") }} total, {{ megabytes (index .Meminfo \"MemAvailable\") }} available<br>\n<strong>resident set size (RSS) by service</strong>:\n<div class=\"progress\">\n\n{{ with $rss := initRss }}\n<div class=\"progress-bar\" style=\"width: {{ rssPercentage $.Meminfo $rss }}%\" title=\"init uses {{ megabytes $rss }} RSS\">\n<span class=\"sr-only\"></span>\ninit\n</div>\n{{ end }}\n\n{{ range $idx, $svc := .Services }}\n{{ with $rss := $svc.RSS }}\n<div class=\"progress-bar\" style=\"width: {{ rssPercentage $.Meminfo $rss }}%\" title=\"{{ $svc.Name }} uses {{ megabytes $rss }} RSS\">\n<span class=\"sr-only\"></span>\n{{ baseName $svc.Name }}\n</div>\n{{ end }}\n{{ end }}\n<div class=\"progress-bar\" style=\"width: 100%; overflow:initial; float: none\" title=\"memory usage outside of gokrazy services\">\n<span class=\"sr-only\"></span>\nunaccounted\n</div>\n</div>\n</div>\n\n<div class=\"col-md-12\">\n\n\n<h2>storage</h2>\n\n{{ if eq .PermAvail 0 }}\nNo permanent storage mounted. To create a filesystem for permanent storage, plug the SD card into a Linux computer and, if your SD card is <code>/dev/sdb</code>, use <code>mkfs.ext4 /dev/sdb4</code>.\n{{ else }}\n<strong>/dev/mmcblk0p4</strong>: {{ gigabytes .PermTotal }} total, {{ gigabytes .PermUsed }} used, {{ gigabytes .PermAvail }} avail<br>\n{{ end }}\n\n<h2>private network addresses</h2>\n<ul>\n{{ range $idx, $host := .Hosts }} \n<li>{{ $host }}</li>\n{{ end }}\n</ul>\n\n</div>\n</div>\n\n{{ template \"footer\" . }}")
var assets_3 = []byte("{{ template \"header\" . }}\n\n<div class=\"row\">\n<div class=\"col-md-12\">\n<table>\n<tr>\n<th>Name</th>\n<th>Started</th>\n<th>Actions</th>\n</tr>\n<tr>\n<td><a href=\"#{{ .Service.Name }}\">{{ .Service.Name }}</a></td>\n<td>{{ .Service.Started }}</td>\n<td><form method=\"POST\" action=\"/restart\"><input type=\"hidden\" name=\"path\" value=\"{{ .Service.Name }}\"><input type=\"submit\" value=\"restart\"></form><form method=\"POST\" action=\"/stop\"><input type=\"hidden\" name=\"path\" value=\"{{ .Service.Name }}\"><input type=\"submit\" value=\"stop\"></form></td>\n</tr>\n</table>\n\n <h3>stdout</h3>\n <pre>\n {{ range $idx, $line := .Service.Stdout.Lines -}}\n {{ $line }}\n {{ end }}\n </pre>\n\n <h3>stderr</h3>\n <pre>\n {{ range $idx, $line := .Service.Stderr.Lines -}}\n {{ $line }}\n {{ end }}\n </pre>\n</div>\n</div>\n\n{{ template \"footer\" . }}")

View File

@ -0,0 +1,5 @@
package bundled
func Asset(basename string) string {
return string(assets["assets/"+basename])
}

124
internal/iface/iface.go Normal file
View File

@ -0,0 +1,124 @@
package iface
import (
"net"
"syscall"
"unsafe"
)
// as per https://manpages.debian.org/jessie/manpages/netdevice.7.en.html
type ifreqAddr struct {
name [16]byte
addr syscall.RawSockaddrInet4
pad [8]byte
}
// as per https://manpages.debian.org/jessie/manpages/netdevice.7.en.html
type ifreqFlags struct {
name [16]byte
flags uint16
pad [22]byte
}
// as per http://lxr.free-electrons.com/source/include/uapi/linux/route.h#L30
type rtentry struct {
pad1 uint64
dst syscall.RawSockaddrInet4
gateway syscall.RawSockaddrInet4
genmask syscall.RawSockaddrInet4
flags uint16
pad [512]byte
}
type Configsocket struct {
fd int
name [16]byte
}
func NewConfigSocket(iface string) (Configsocket, error) {
fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_DGRAM, 0)
if err != nil {
return Configsocket{}, err
}
cs := Configsocket{
fd: fd,
}
copy(cs.name[:], []byte(iface))
return cs, nil
}
func (cs Configsocket) Close() error {
return syscall.Close(cs.fd)
}
func (cs Configsocket) ifreqAddr(request uintptr, addr net.IP) error {
req := ifreqAddr{
name: cs.name,
addr: syscall.RawSockaddrInet4{
Family: syscall.AF_INET,
},
}
copy(req.addr.Addr[:], addr)
if _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(cs.fd), request, uintptr(unsafe.Pointer(&req))); errno != 0 {
return errno
}
return nil
}
func (cs Configsocket) SetAddress(addr net.IP) error {
return cs.ifreqAddr(syscall.SIOCSIFADDR, addr)
}
func (cs Configsocket) SetNetmask(addr net.IPMask) error {
return cs.ifreqAddr(syscall.SIOCSIFNETMASK, net.IP(addr))
}
func (cs Configsocket) SetBroadcast(addr net.IP) error {
return cs.ifreqAddr(syscall.SIOCSIFBRDADDR, addr)
}
func (cs Configsocket) Up() error {
req := ifreqFlags{name: cs.name}
if _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(cs.fd), syscall.SIOCGIFFLAGS, uintptr(unsafe.Pointer(&req))); errno != 0 {
return errno
}
req.flags |= syscall.IFF_UP
req.flags |= syscall.IFF_RUNNING
if _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(cs.fd), syscall.SIOCSIFFLAGS, uintptr(unsafe.Pointer(&req))); errno != 0 {
return errno
}
return nil
}
func (cs Configsocket) AddRoute(dst, gateway net.IP, genmask net.IPMask) syscall.Errno {
req := rtentry{
dst: syscall.RawSockaddrInet4{Family: syscall.AF_INET},
gateway: syscall.RawSockaddrInet4{Family: syscall.AF_INET},
genmask: syscall.RawSockaddrInet4{Family: syscall.AF_INET},
flags: syscall.RTF_UP | syscall.RTF_GATEWAY,
}
copy(req.dst.Addr[:], dst)
copy(req.gateway.Addr[:], gateway)
copy(req.genmask.Addr[:], genmask)
_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(cs.fd), syscall.SIOCADDRT, uintptr(unsafe.Pointer(&req)))
return errno
}
func (cs Configsocket) DelRoute(dst, gateway net.IP, genmask net.IPMask) syscall.Errno {
req := rtentry{
dst: syscall.RawSockaddrInet4{Family: syscall.AF_INET},
gateway: syscall.RawSockaddrInet4{Family: syscall.AF_INET},
genmask: syscall.RawSockaddrInet4{Family: syscall.AF_INET},
flags: syscall.RTF_UP | syscall.RTF_GATEWAY,
}
copy(req.dst.Addr[:], dst)
copy(req.gateway.Addr[:], gateway)
copy(req.genmask.Addr[:], genmask)
_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(cs.fd), syscall.SIOCDELRT, uintptr(unsafe.Pointer(&req)))
return errno
}

135
listeners.go Normal file
View File

@ -0,0 +1,135 @@
package gokrazy
import (
"log"
"net"
"net/http"
"sync"
)
var privateNets []net.IPNet
var ipv6LinkLocal net.IPNet
func init() {
var pn = []string{
// loopback: https://tools.ietf.org/html/rfc3330#section-2
"127.0.0.0/8",
// loopback: https://tools.ietf.org/html/rfc3513#section-2.4
"::1/128",
// reserved: https://tools.ietf.org/html/rfc1918#section-3
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16",
// reserved: https://tools.ietf.org/html/rfc4193#section-3.1
"fc00::/7",
// link-local: https://tools.ietf.org/html/rfc3927#section-1.2
"169.254.0.0/16",
// link-local: https://tools.ietf.org/html/rfc4291#section-2.4
"fe80::/10",
}
privateNets = make([]net.IPNet, len(pn))
for idx, s := range pn {
_, net, err := net.ParseCIDR(s)
if err != nil {
log.Panicf(err.Error())
}
privateNets[idx] = *net
if s == "fe80::/10" {
ipv6LinkLocal = *net
}
}
}
func isPrivate(ipaddr net.IP) bool {
for _, n := range privateNets {
if n.Contains(ipaddr) {
return true
}
}
return false
}
// PrivateInterfaceAddrs returns all private (as per RFC1918, RFC4193,
// RFC3330, RFC3513, RFC3927, RFC4291) host addresses of all active
// interfaces, suitable to be passed to net.Listen.
func PrivateInterfaceAddrs() ([]string, error) {
ifaces, err := net.Interfaces()
if err != nil {
return nil, err
}
var hosts []string
for _, i := range ifaces {
if i.Flags&net.FlagUp != net.FlagUp {
continue
}
addrs, err := i.Addrs()
if err != nil {
return nil, err
}
for _, a := range addrs {
ipaddr, _, err := net.ParseCIDR(a.String())
if err != nil {
return nil, err
}
if !isPrivate(ipaddr) {
continue
}
host := ipaddr.String()
if ipv6LinkLocal.Contains(ipaddr) {
host = host + "%" + i.Name
}
hosts = append(hosts, host)
}
}
return hosts, nil
}
var (
listeners = make(map[string]*http.Server)
listenersMu sync.Mutex
)
func updateListeners(port string) error {
hosts, err := PrivateInterfaceAddrs()
if err != nil {
return err
}
listenersMu.Lock()
defer listenersMu.Unlock()
vanished := make(map[string]bool)
for host := range listeners {
vanished[host] = false
}
for _, host := range hosts {
if _, ok := listeners[host]; ok {
// confirm found
delete(vanished, host)
} else {
// add a new listener
srv := &http.Server{
Addr: net.JoinHostPort(host, port),
Handler: http.HandlerFunc(authenticated),
}
listeners[host] = srv
go func(host string, srv *http.Server) {
err := srv.ListenAndServe()
log.Printf("listener for %q died: %v", host, err)
listenersMu.Lock()
defer listenersMu.Unlock()
delete(listeners, host)
}(host, srv)
}
}
for host := range vanished {
listeners[host].Close()
delete(listeners, host)
}
return nil
}

85
mount.go Normal file
View File

@ -0,0 +1,85 @@
package gokrazy
import (
"fmt"
"io/ioutil"
"log"
"os"
"syscall"
)
func mountfs() error {
if err := syscall.Mount("tmpfs", "/tmp", "tmpfs", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_RELATIME, "size=50M"); err != nil {
return fmt.Errorf("tmpfs on /tmp: %v", err)
}
// Symlink /etc/resolv.conf. We cannot do this in the root file
// system itself, as FAT does not support symlinks.
if err := syscall.Mount("tmpfs", "/etc", "tmpfs", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_RELATIME, "size=1M"); err != nil {
return fmt.Errorf("tmpfs on /etc: %v", err)
}
if err := os.Symlink("/proc/net/pnp", "/etc/resolv.conf"); err != nil {
return fmt.Errorf("etc: %v", err)
}
// Symlink /etc/localtime. We cannot do this in the root file
// system, as FAT filenames are limited to 8.3.
if err := os.Symlink("/localtim", "/etc/localtime"); err != nil {
return fmt.Errorf("etc: %v", err)
}
if err := os.Mkdir("/etc/ssl", 0755); err != nil {
return fmt.Errorf("/etc/ssl: %v", err)
}
if err := os.Symlink("/cacerts", "/etc/ssl/ca-bundle.pem"); err != nil {
return fmt.Errorf("/etc/ssl: %v", err)
}
if err := ioutil.WriteFile("/etc/hosts", []byte("127.0.0.1 localhost\n::1 localhost\n"), 0644); err != nil {
return fmt.Errorf("/etc/hosts: %v", err)
}
if err := syscall.Mount("devtmpfs", "/dev", "devtmpfs", 0, ""); err != nil {
if sce, ok := err.(syscall.Errno); ok && sce == syscall.EBUSY {
// /dev was already mounted (common in setups using nfsroot= or initramfs)
} else {
return fmt.Errorf("devtmpfs: %v", err)
}
}
if err := os.MkdirAll("/dev/pts", 0755); err != nil {
return fmt.Errorf("mkdir /dev/pts: %v", err)
}
if err := syscall.Mount("devpts", "/dev/pts", "devpts", 0, ""); err != nil {
return fmt.Errorf("devpts: %v", err)
}
// /proc is useful for exposing process details and for
// interactive debugging sessions.
if err := syscall.Mount("proc", "/proc", "proc", 0, ""); err != nil {
if sce, ok := err.(syscall.Errno); ok && sce == syscall.EBUSY {
// /proc was already mounted (common in setups using nfsroot= or initramfs)
} else {
return fmt.Errorf("proc: %v", err)
}
}
// /sys is useful for retrieving additional status from the
// kernel, e.g. ethernet device carrier status.
if err := syscall.Mount("sysfs", "/sys", "sysfs", 0, ""); err != nil {
if sce, ok := err.(syscall.Errno); ok && sce == syscall.EBUSY {
// /sys was already mounted (common in setups using nfsroot= or initramfs)
} else {
return fmt.Errorf("sys: %v", err)
}
}
if err := syscall.Mount("/dev/mmcblk0p4", "/perm", "ext4", 0, ""); err != nil {
log.Printf("Could not mount permanent storage partition: %v", err)
}
return nil
}

156
status.go Normal file
View File

@ -0,0 +1,156 @@
package gokrazy
import (
"bytes"
"fmt"
"html/template"
"io"
"io/ioutil"
"log"
"net/http"
"path/filepath"
"sort"
"strconv"
"strings"
"time"
"github.com/gokrazy/gokrazy/internal/bundled"
"golang.org/x/sys/unix"
)
func parseMeminfo() map[string]int64 {
meminfo, err := ioutil.ReadFile("/proc/meminfo")
if err != nil {
return nil
}
vals := make(map[string]int64)
for _, line := range strings.Split(string(meminfo), "\n") {
if !strings.HasPrefix(line, "MemTotal") &&
!strings.HasPrefix(line, "MemAvailable") {
continue
}
parts := strings.Split(line, ":")
if len(parts) < 2 {
continue
}
val, err := strconv.ParseInt(strings.TrimSpace(strings.TrimSuffix(parts[1], " kB")), 0, 64)
if err != nil {
continue
}
vals[parts[0]] = val * 1024 // KiB to B
}
return vals
}
var commonTmpls = mustParseCommonTmpls()
func mustParseCommonTmpls() *template.Template {
t := template.New("root")
t = template.Must(t.New("header").Parse(bundled.Asset("header.tmpl")))
t = template.Must(t.New("footer").Parse(bundled.Asset("footer.tmpl")))
return t
}
var overviewTmpl = template.Must(template.Must(commonTmpls.Clone()).New("overview").
Funcs(map[string]interface{}{
"restarting": func(t time.Time) bool {
return time.Since(t).Seconds() < 5
},
"last": func(stdout, stderr []string) string {
if len(stdout) == 0 && len(stderr) == 0 {
return ""
}
both := append(stdout, stderr...)
sort.Strings(both)
return both[len(both)-1]
},
"megabytes": func(val int64) string {
return fmt.Sprintf("%.1f MiB", float64(val)/1024/1024)
},
"gigabytes": func(val int64) string {
return fmt.Sprintf("%.1f GiB", float64(val)/1024/1024/1024)
},
"baseName": func(path string) string {
return filepath.Base(path)
},
"initRss": func() int64 {
return rssOfPid(1)
},
"rssPercentage": func(meminfo map[string]int64, rss int64) string {
used := float64(meminfo["MemTotal"] - meminfo["MemAvailable"])
return fmt.Sprintf("%.f", float64(rss)/used*100)
},
}).
Parse(bundled.Asset("overview.tmpl")))
var statusTmpl = template.Must(template.Must(commonTmpls.Clone()).New("statusTmpl").Parse(bundled.Asset("status.tmpl")))
func initStatus(services []*service) {
http.HandleFunc("/status", func(w http.ResponseWriter, r *http.Request) {
path := r.FormValue("path")
var svc *service
for _, s := range services {
if s.cmd.Path != path {
continue
}
svc = s
break
}
var buf bytes.Buffer
if err := statusTmpl.Execute(&buf, struct {
Service *service
BuildTimestamp string
Hostname string
}{
Service: svc,
BuildTimestamp: buildTimestamp,
Hostname: hostname,
}); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
io.Copy(w, &buf)
})
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
var st unix.Statfs_t
if err := unix.Statfs("/perm", &st); err != nil {
log.Printf("could not stat /perm: %v", err)
}
hosts, err := PrivateInterfaceAddrs()
if err != nil {
log.Printf("could not get addrs: %v", err)
}
var buf bytes.Buffer
if err := overviewTmpl.Execute(&buf, struct {
Services []*service
PermUsed int64
PermAvail int64
PermTotal int64
Hosts []string
BuildTimestamp string
Meminfo map[string]int64
Hostname string
}{
Services: services,
PermUsed: int64(st.Bsize) * int64(st.Blocks-st.Bfree),
PermAvail: int64(st.Bsize) * int64(st.Bavail),
PermTotal: int64(st.Bsize) * int64(st.Blocks),
Hosts: hosts,
BuildTimestamp: buildTimestamp,
Meminfo: parseMeminfo(),
Hostname: hostname,
}); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
io.Copy(w, &buf)
})
}

290
supervise.go Normal file
View File

@ -0,0 +1,290 @@
package gokrazy
import (
"container/ring"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
"os"
"os/exec"
"strconv"
"strings"
"sync"
"syscall"
"time"
)
type lineRingBuffer struct {
sync.RWMutex
remainder string
r *ring.Ring
}
func newLineRingBuffer(size int) *lineRingBuffer {
return &lineRingBuffer{
r: ring.New(size),
}
}
func (lrb *lineRingBuffer) Write(b []byte) (int, error) {
lrb.Lock()
defer lrb.Unlock()
text := lrb.remainder + string(b)
for {
idx := strings.Index(text, "\n")
if idx == -1 {
break
}
lrb.r.Value = text[:idx]
lrb.r = lrb.r.Next()
text = text[idx+1:]
}
lrb.remainder = text
return len(b), nil
}
func (lrb *lineRingBuffer) Lines() []string {
lrb.RLock()
defer lrb.RUnlock()
lines := make([]string, 0, lrb.r.Len())
lrb.r.Do(func(x interface{}) {
if x != nil {
lines = append(lines, x.(string))
}
})
return lines
}
type service struct {
stopped bool
stoppedMu sync.RWMutex
cmd *exec.Cmd
Stdout *lineRingBuffer
Stderr *lineRingBuffer
started time.Time
startedMu sync.RWMutex
attempt uint64
process *os.Process
processMu sync.RWMutex
}
func (s *service) Name() string {
return s.cmd.Args[0]
}
func (s *service) Stopped() bool {
s.stoppedMu.RLock()
defer s.stoppedMu.RUnlock()
return s.stopped
}
func (s *service) setStopped(val bool) {
s.stoppedMu.Lock()
defer s.stoppedMu.Unlock()
s.stopped = val
}
func (s *service) Started() time.Time {
s.startedMu.RLock()
defer s.startedMu.RUnlock()
return s.started
}
func (s *service) setStarted(t time.Time) {
s.startedMu.Lock()
defer s.startedMu.Unlock()
s.started = t
}
func (s *service) Process() *os.Process {
s.processMu.RLock()
defer s.processMu.RUnlock()
return s.process
}
func (s *service) setProcess(p *os.Process) {
s.processMu.Lock()
defer s.processMu.Unlock()
s.process = p
}
func rssOfPid(pid int) int64 {
statm, err := ioutil.ReadFile(fmt.Sprintf("/proc/%d/statm", pid))
if err != nil {
return 0
}
parts := strings.Split(strings.TrimSpace(string(statm)), " ")
if len(parts) < 2 {
return 0
}
rss, err := strconv.ParseInt(parts[1], 0, 64)
if err != nil {
return 0
}
return rss * 4096
}
func (s *service) RSS() int64 {
if p := s.Process(); p != nil {
return rssOfPid(s.Process().Pid)
}
return 0
}
func isDontSupervise(err error) bool {
ee, ok := err.(*exec.ExitError)
if !ok {
return false
}
ws, ok := ee.Sys().(syscall.WaitStatus)
if !ok {
return false
}
return ws.ExitStatus() == 125
}
func supervise(s *service) {
s.Stdout = newLineRingBuffer(100)
s.Stderr = newLineRingBuffer(100)
l := log.New(s.Stderr, "", log.LstdFlags|log.Ldate|log.Ltime)
attempt := 0
for {
if s.Stopped() {
time.Sleep(1 * time.Second)
continue
}
l.Printf("gokrazy: attempt %d, starting %+v", attempt, s.cmd.Args)
s.setStarted(time.Now())
cmd := &exec.Cmd{
Path: s.cmd.Path,
Args: s.cmd.Args,
Stdout: s.Stdout,
Stderr: s.Stderr,
SysProcAttr: &syscall.SysProcAttr{
Unshareflags: syscall.CLONE_NEWNS,
},
}
if attempt == 0 {
cmd.Env = append(cmd.Env, "GOKRAZY_FIRST_START=1")
}
attempt++
if err := cmd.Start(); err != nil {
l.Println("gokrazy: " + err.Error())
}
s.setProcess(cmd.Process)
if err := cmd.Wait(); err != nil {
if isDontSupervise(err) {
l.Println("gokrazy: process should not be supervised, stopping")
s.setStopped(true)
}
l.Println("gokrazy: " + err.Error())
} else {
l.Printf("gokrazy: exited successfully")
}
time.Sleep(1 * time.Second)
}
}
func redirectToStatus(w http.ResponseWriter, r *http.Request, path string) {
// StatusSeeOther will result in a GET request for the
// redirect location
u, _ := url.Parse("/status")
u.RawQuery = url.Values{
"path": []string{path},
}.Encode()
http.Redirect(w, r, u.String(), http.StatusSeeOther)
}
func superviseServices(services []*service) {
for _, s := range services {
go supervise(s)
}
findSvc := func(path string) *service {
for _, s := range services {
if s.cmd.Path == path {
return s
}
}
return nil
}
http.HandleFunc("/stop", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "expected a POST request", http.StatusBadRequest)
return
}
path := r.FormValue("path")
s := findSvc(path)
if s == nil || s.Stopped() {
redirectToStatus(w, r, path)
return
}
s.setStopped(true)
p := s.Process()
if p == nil {
redirectToStatus(w, r, path)
return
}
if err := p.Signal(syscall.SIGTERM); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
redirectToStatus(w, r, path)
})
http.HandleFunc("/restart", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "expected a POST request", http.StatusBadRequest)
return
}
signal := syscall.SIGTERM
if r.FormValue("signal") == "kill" {
signal = syscall.SIGKILL
}
path := r.FormValue("path")
s := findSvc(path)
if s == nil {
redirectToStatus(w, r, path)
return
}
if s.Stopped() {
s.setStopped(false)
redirectToStatus(w, r, path)
return
}
p := s.Process()
if p == nil {
redirectToStatus(w, r, path)
return
}
if err := p.Signal(signal); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
redirectToStatus(w, r, path)
})
}

154
update.go Normal file
View File

@ -0,0 +1,154 @@
package gokrazy
import (
"crypto/sha256"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"regexp"
"sync"
"syscall"
"time"
"golang.org/x/sys/unix"
"github.com/gokrazy/fat"
)
var rootRe = regexp.MustCompile(`root=/dev/mmcblk0p([2-3])`)
func switchRootPartition(newRootPartition string) error {
f, err := os.OpenFile("/dev/mmcblk0p1", os.O_RDWR, 0600)
if err != nil {
return err
}
defer f.Close()
rd, err := fat.NewReader(f)
if err != nil {
return err
}
offset, length, err := rd.Extents("/cmdline.txt")
if err != nil {
return err
}
if _, err := f.Seek(offset, io.SeekStart); err != nil {
return err
}
b := make([]byte, length)
if _, err := f.Read(b); err != nil {
return err
}
if _, err := f.Seek(offset, io.SeekStart); err != nil {
return err
}
rep := rootRe.ReplaceAllLiteral(b, []byte("root=/dev/mmcblk0p"+newRootPartition))
if _, err := f.Write(rep); err != nil {
return err
}
return f.Close()
}
func streamRequestTo(path string, r io.Reader) error {
f, err := os.OpenFile(path, os.O_WRONLY, 0600)
if err != nil {
return err
}
defer f.Close()
if _, err := io.Copy(f, r); err != nil {
return err
}
return f.Close()
}
func nonConcurrentUpdateHandler(dest string) func(http.ResponseWriter, *http.Request) {
var mu sync.Mutex
return func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPut {
http.Error(w, "expected a PUT request", http.StatusBadRequest)
return
}
mu.Lock()
defer mu.Unlock()
hash := sha256.New()
if err := streamRequestTo(dest, io.TeeReader(r.Body, hash)); err != nil {
log.Printf("updating %q failed: %v", dest, err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
fmt.Fprintf(w, "%x", hash.Sum(nil))
}
}
func nonConcurrentSwitchHandler(newRootPartition string) func(http.ResponseWriter, *http.Request) {
var mu sync.Mutex
return func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "expected a POST request", http.StatusBadRequest)
return
}
mu.Lock()
defer mu.Unlock()
if err := switchRootPartition(newRootPartition); err != nil {
log.Printf("switching root partition to %q failed: %v", newRootPartition, err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
}
func initUpdate() error {
cmdline, err := ioutil.ReadFile("/proc/cmdline")
if err != nil {
return err
}
matches := rootRe.FindStringSubmatch(string(cmdline))
if matches == nil {
return fmt.Errorf("identify 2/3 partition: kernel command line %q did not match %v", string(cmdline), rootRe)
}
rootPartition := matches[1]
var inactiveRootPartition string
switch rootPartition {
case "2":
inactiveRootPartition = "3"
case "3":
inactiveRootPartition = "2"
default:
return fmt.Errorf("root partition %q (from %q) is unexpectedly neither 2 nor 3", rootPartition, matches[0])
}
http.HandleFunc("/update/boot", nonConcurrentUpdateHandler("/dev/mmcblk0p1"))
http.HandleFunc("/update/root", nonConcurrentUpdateHandler("/dev/mmcblk0p"+inactiveRootPartition))
http.HandleFunc("/update/switch", nonConcurrentSwitchHandler(inactiveRootPartition))
http.HandleFunc("/reboot", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "expected a POST request", http.StatusBadRequest)
return
}
// TODO: implement a shutdown sequence, i.e. kill + timeout + term; unmount, then force-unmount?
if err := syscall.Unmount("/perm", unix.MNT_FORCE); err != nil {
log.Printf("unmounting /perm failed: %v", err)
}
go func() {
time.Sleep(1 * time.Second) // give the HTTP response some time to be sent
if err := unix.Reboot(unix.LINUX_REBOOT_CMD_RESTART); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}()
})
return nil
}