Initial commit
This commit is contained in:
commit
38af7fd18d
27
LICENSE
Normal file
27
LICENSE
Normal 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
121
README.md
Normal 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 haven’t 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 you’d like to store permanent data (i.e. data which will not be
|
||||||
|
overwritten on the next update), you’ll 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
7
assets/footer.tmpl
Normal 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
57
assets/header.tmpl
Normal 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
82
assets/overview.tmpl
Normal 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
34
assets/status.tmpl
Normal 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
38
authenticated.go
Normal 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
3
bundle.go
Normal 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
233
cmd/dhcp/dhcp.go
Normal 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
32
cmd/ntp/ntp.go
Normal 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
4
doc.go
Normal 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
190
goembed.go
Normal 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
159
gokrazy.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
13
internal/bundled/GENERATED_bundled.go
Normal file
13
internal/bundled/GENERATED_bundled.go
Normal 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\" . }}")
|
5
internal/bundled/bundled.go
Normal file
5
internal/bundled/bundled.go
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
package bundled
|
||||||
|
|
||||||
|
func Asset(basename string) string {
|
||||||
|
return string(assets["assets/"+basename])
|
||||||
|
}
|
124
internal/iface/iface.go
Normal file
124
internal/iface/iface.go
Normal 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
135
listeners.go
Normal 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
85
mount.go
Normal 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
156
status.go
Normal 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
290
supervise.go
Normal 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
154
update.go
Normal 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
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user