Compare commits
1 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
985d2475dc |
2
.github/workflows/main.yml
vendored
2
.github/workflows/main.yml
vendored
@ -15,7 +15,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Ensure all files were formatted as per gofmt
|
- name: Ensure all files were formatted as per gofmt
|
||||||
run: |
|
run: |
|
||||||
[ "$(gofmt -l $(find . -name '*.go') 2>&1)" = "" ]
|
gofmt -l $(find . -name '*.go') >/dev/null
|
||||||
|
|
||||||
- name: run tests
|
- name: run tests
|
||||||
run: go test ./...
|
run: go test ./...
|
||||||
|
@ -52,10 +52,6 @@ table {
|
|||||||
<th>host</th>
|
<th>host</th>
|
||||||
<td>{{ .Hostname }}</td>
|
<td>{{ .Hostname }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<th>kernel</th>
|
|
||||||
<td>{{ .Kernel }}</td>
|
|
||||||
</tr>
|
|
||||||
{{ if (ne .Model "") }}
|
{{ if (ne .Model "") }}
|
||||||
<tr>
|
<tr>
|
||||||
<th>model</th>
|
<th>model</th>
|
||||||
|
@ -8,10 +8,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func authenticated(w http.ResponseWriter, r *http.Request) {
|
func authenticated(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.URL.Path == "/test" {
|
|
||||||
w.Write([]byte("It's working!"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// defense in depth
|
// defense in depth
|
||||||
if httpPassword == "" {
|
if httpPassword == "" {
|
||||||
http.Error(w, "httpPassword not set", http.StatusInternalServerError)
|
http.Error(w, "httpPassword not set", http.StatusInternalServerError)
|
||||||
|
@ -9,7 +9,6 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
@ -21,11 +20,8 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"internal/notify"
|
|
||||||
|
|
||||||
"github.com/gokrazy/gokrazy/internal/iface"
|
"github.com/gokrazy/gokrazy/internal/iface"
|
||||||
"github.com/google/gopacket/layers"
|
"github.com/google/gopacket/layers"
|
||||||
"github.com/google/renameio"
|
|
||||||
"github.com/mdlayher/raw"
|
"github.com/mdlayher/raw"
|
||||||
"github.com/rtr7/dhcp4"
|
"github.com/rtr7/dhcp4"
|
||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
@ -68,9 +64,7 @@ func (c *client) discover() (*layers.DHCPv4, error) {
|
|||||||
layers.DHCPOptDNS,
|
layers.DHCPOptDNS,
|
||||||
layers.DHCPOptRouter,
|
layers.DHCPOptRouter,
|
||||||
layers.DHCPOptSubnetMask,
|
layers.DHCPOptSubnetMask,
|
||||||
layers.DHCPOptDomainName,
|
layers.DHCPOptDomainName),
|
||||||
layers.DHCPOptNTPServers,
|
|
||||||
),
|
|
||||||
})
|
})
|
||||||
if err := dhcp4.Write(c.conn, discover); err != nil {
|
if err := dhcp4.Write(c.conn, discover); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -107,9 +101,7 @@ func (c *client) request(last *layers.DHCPv4) (*layers.DHCPv4, error) {
|
|||||||
layers.DHCPOptDNS,
|
layers.DHCPOptDNS,
|
||||||
layers.DHCPOptRouter,
|
layers.DHCPOptRouter,
|
||||||
layers.DHCPOptSubnetMask,
|
layers.DHCPOptSubnetMask,
|
||||||
layers.DHCPOptDomainName,
|
layers.DHCPOptDomainName),
|
||||||
layers.DHCPOptNTPServers,
|
|
||||||
),
|
|
||||||
}, dhcp4.ServerID(last.Options)...))
|
}, dhcp4.ServerID(last.Options)...))
|
||||||
if err := dhcp4.Write(c.conn, request); err != nil {
|
if err := dhcp4.Write(c.conn, request); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -135,11 +127,6 @@ func (c *client) request(last *layers.DHCPv4) (*layers.DHCPv4, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var permDir = flag.String(
|
|
||||||
"perm",
|
|
||||||
"/perm",
|
|
||||||
"path to replace /perm")
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||||
var (
|
var (
|
||||||
@ -150,10 +137,6 @@ func main() {
|
|||||||
)
|
)
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if err := os.MkdirAll(filepath.Join(*permDir, "dhcp4"), 0755); err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NOTE: cannot gokrazy.WaitForClock() here, since the clock can only be
|
// NOTE: cannot gokrazy.WaitForClock() here, since the clock can only be
|
||||||
// initialized once the network is up.
|
// initialized once the network is up.
|
||||||
|
|
||||||
@ -214,7 +197,6 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
lease := dhcp4.LeaseFromACK(last)
|
lease := dhcp4.LeaseFromACK(last)
|
||||||
saveLease(lease)
|
|
||||||
|
|
||||||
// Log the received DHCPACK packet:
|
// Log the received DHCPACK packet:
|
||||||
details := []string{
|
details := []string{
|
||||||
@ -236,9 +218,6 @@ func main() {
|
|||||||
if len(lease.Broadcast) > 0 {
|
if len(lease.Broadcast) > 0 {
|
||||||
details = append(details, fmt.Sprintf("broadcast %v", lease.Broadcast))
|
details = append(details, fmt.Sprintf("broadcast %v", lease.Broadcast))
|
||||||
}
|
}
|
||||||
if len(lease.NTP) > 0 {
|
|
||||||
details = append(details, fmt.Sprintf("NTP %v", lease.NTP))
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("DHCPACK: %v", strings.Join(details, ", "))
|
log.Printf("DHCPACK: %v", strings.Join(details, ", "))
|
||||||
|
|
||||||
@ -307,22 +286,3 @@ func main() {
|
|||||||
time.Sleep(lease.RenewalTime)
|
time.Sleep(lease.RenewalTime)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveLease(lease dhcp4.Lease) {
|
|
||||||
b, err := json.Marshal(lease)
|
|
||||||
if err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var out bytes.Buffer
|
|
||||||
if err := json.Indent(&out, b, "", "\t"); err == nil {
|
|
||||||
b = out.Bytes()
|
|
||||||
}
|
|
||||||
if err := renameio.WriteFile(filepath.Join(*permDir, "dhcp4/lease.json"), b, 0644); err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := notify.Process(filepath.Join(filepath.Dir(os.Args[0]), "/ntp"), syscall.SIGUSR1); err != nil {
|
|
||||||
log.Printf("notifying ntp: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -2,28 +2,17 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"flag"
|
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
"log"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net"
|
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
|
||||||
"path/filepath"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/beevik/ntp"
|
"github.com/beevik/ntp"
|
||||||
"github.com/rtr7/dhcp4"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var server = "0.gokrazy.pool.ntp.org"
|
|
||||||
|
|
||||||
var permDir = flag.String("perm", "/perm", "path to replace /perm")
|
|
||||||
|
|
||||||
func set(rtc *os.File) error {
|
func set(rtc *os.File) error {
|
||||||
r, err := ntp.Query(server)
|
r, err := ntp.Query("0.gokrazy.pool.ntp.org")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -32,7 +21,7 @@ func set(rtc *os.File) error {
|
|||||||
if err := syscall.Settimeofday(&tv); err != nil {
|
if err := syscall.Settimeofday(&tv); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
log.Printf("clock set to %v using %v", r.Time, server)
|
log.Printf("clock set to %v", r.Time)
|
||||||
|
|
||||||
if rtc == nil {
|
if rtc == nil {
|
||||||
return nil
|
return nil
|
||||||
@ -41,7 +30,6 @@ func set(rtc *os.File) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
flag.Parse()
|
|
||||||
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||||
|
|
||||||
var rtc *os.File
|
var rtc *os.File
|
||||||
@ -59,15 +47,6 @@ func main() {
|
|||||||
|
|
||||||
mustDropPrivileges(rtc)
|
mustDropPrivileges(rtc)
|
||||||
|
|
||||||
go func() {
|
|
||||||
ch := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(ch, syscall.SIGUSR1)
|
|
||||||
for range ch {
|
|
||||||
loadNTPServer()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
loadNTPServer()
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
if err := set(rtc); err != nil {
|
if err := set(rtc); err != nil {
|
||||||
log.Fatalf("setting time failed: %v", err)
|
log.Fatalf("setting time failed: %v", err)
|
||||||
@ -75,21 +54,3 @@ func main() {
|
|||||||
time.Sleep(1*time.Hour + time.Duration(rand.Int63n(250))*time.Millisecond)
|
time.Sleep(1*time.Hour + time.Duration(rand.Int63n(250))*time.Millisecond)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadNTPServer() {
|
|
||||||
var lease dhcp4.Lease
|
|
||||||
file, err := ioutil.ReadFile(filepath.Join(*permDir, "dhcp4/lease.json"))
|
|
||||||
if err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err = json.Unmarshal(file, &lease)
|
|
||||||
if err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if len(lease.DNS) > 0 && !lease.DNS[0].To4().Equal(net.IPv4zero) {
|
|
||||||
server = lease.DNS[0].String()
|
|
||||||
log.Printf("Setting ntp server to: %s", server)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -7,7 +7,6 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"os/signal"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
)
|
)
|
||||||
@ -85,14 +84,5 @@ func mustDropPrivileges(rtc *os.File) {
|
|||||||
},
|
},
|
||||||
AmbientCaps: []uintptr{CAP_SYS_TIME},
|
AmbientCaps: []uintptr{CAP_SYS_TIME},
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
|
||||||
ch := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(ch)
|
|
||||||
for sig := range ch {
|
|
||||||
cmd.Process.Signal(sig)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
log.Fatal(cmd.Run())
|
log.Fatal(cmd.Run())
|
||||||
}
|
}
|
||||||
|
@ -85,11 +85,6 @@ Drive.
|
|||||||
beatbox is a Raspberry Pi 3-based toy that combines a Mir:ror and NFC
|
beatbox is a Raspberry Pi 3-based toy that combines a Mir:ror and NFC
|
||||||
figurines for playing music stored on the device or directly from Spotify.
|
figurines for playing music stored on the device or directly from Spotify.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
|
||||||
<a href="https://github.com/mdlayher/consrv"><strong>consrv</strong></a><br>
|
|
||||||
consrv is a Raspberry Pi 4-based appliance that provides a basic SSH to
|
|
||||||
serial console bridge for accessing remote devices.
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
|
6
go.mod
6
go.mod
@ -7,12 +7,10 @@ require (
|
|||||||
github.com/gokrazy/internal v0.0.0-20200531194636-d96421c60091
|
github.com/gokrazy/internal v0.0.0-20200531194636-d96421c60091
|
||||||
github.com/google/go-cmp v0.4.0 // indirect
|
github.com/google/go-cmp v0.4.0 // indirect
|
||||||
github.com/google/gopacket v1.1.16
|
github.com/google/gopacket v1.1.16
|
||||||
github.com/google/renameio v0.1.0
|
|
||||||
github.com/mdlayher/raw v0.0.0-20190303161257-764d452d77af
|
github.com/mdlayher/raw v0.0.0-20190303161257-764d452d77af
|
||||||
github.com/mdlayher/watchdog v0.0.0-20201005150459-8bdc4f41966b
|
|
||||||
github.com/rtr7/dhcp4 v0.0.0-20181120124042-778e8c2e24a5
|
github.com/rtr7/dhcp4 v0.0.0-20181120124042-778e8c2e24a5
|
||||||
github.com/stretchr/testify v1.5.1 // indirect
|
github.com/stretchr/testify v1.5.1 // indirect
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 // indirect
|
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413
|
||||||
golang.org/x/sys v0.0.0-20201005065044-765f4ea38db3
|
golang.org/x/sys v0.0.0-20200406155108-e3b113bbe6a4
|
||||||
rsc.io/goversion v1.2.0
|
rsc.io/goversion v1.2.0
|
||||||
)
|
)
|
||||||
|
15
go.sum
15
go.sum
@ -2,18 +2,19 @@ github.com/beevik/ntp v0.2.0 h1:sGsd+kAXzT0bfVfzJfce04g+dSRfrs+tbQW8lweuYgw=
|
|||||||
github.com/beevik/ntp v0.2.0/go.mod h1:hIHWr+l3+/clUnF44zdK+CWW7fO8dR5cIylAQ76NRpg=
|
github.com/beevik/ntp v0.2.0/go.mod h1:hIHWr+l3+/clUnF44zdK+CWW7fO8dR5cIylAQ76NRpg=
|
||||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/gokrazy/internal v0.0.0-20200407065509-37efc446ad44/go.mod h1:LA5TQy7LcvYGQOy75tkrYkFUhbV2nl5qEBP47PSi2JA=
|
||||||
|
github.com/gokrazy/internal v0.0.0-20200407075822-660ad467b7c9 h1:x5jR/nNo4/kMSoNo/nwa2xbL7PN1an8S3oIn4OZJdec=
|
||||||
|
github.com/gokrazy/internal v0.0.0-20200407075822-660ad467b7c9/go.mod h1:LA5TQy7LcvYGQOy75tkrYkFUhbV2nl5qEBP47PSi2JA=
|
||||||
|
github.com/gokrazy/internal v0.0.0-20200530170418-389acc6d0821 h1:H8TFA9ZURF3PrJAhMCvG9uUDrxhrHtULYz4V6qPJcnc=
|
||||||
|
github.com/gokrazy/internal v0.0.0-20200530170418-389acc6d0821/go.mod h1:LA5TQy7LcvYGQOy75tkrYkFUhbV2nl5qEBP47PSi2JA=
|
||||||
github.com/gokrazy/internal v0.0.0-20200531194636-d96421c60091 h1:gP2Z4WgsQl35mlNf4kqYW0D8KnYMC4kdsczagvVKBbg=
|
github.com/gokrazy/internal v0.0.0-20200531194636-d96421c60091 h1:gP2Z4WgsQl35mlNf4kqYW0D8KnYMC4kdsczagvVKBbg=
|
||||||
github.com/gokrazy/internal v0.0.0-20200531194636-d96421c60091/go.mod h1:LA5TQy7LcvYGQOy75tkrYkFUhbV2nl5qEBP47PSi2JA=
|
github.com/gokrazy/internal v0.0.0-20200531194636-d96421c60091/go.mod h1:LA5TQy7LcvYGQOy75tkrYkFUhbV2nl5qEBP47PSi2JA=
|
||||||
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
|
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
|
||||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/gopacket v1.1.16 h1:u6Afvia5C5srlLcbTwpHaFW918asLYPxieziOaWwz8M=
|
github.com/google/gopacket v1.1.16 h1:u6Afvia5C5srlLcbTwpHaFW918asLYPxieziOaWwz8M=
|
||||||
github.com/google/gopacket v1.1.16/go.mod h1:UCLx9mCmAwsVbn6qQl1WIEt2SO7Nd2fD0th1TBAsqBw=
|
github.com/google/gopacket v1.1.16/go.mod h1:UCLx9mCmAwsVbn6qQl1WIEt2SO7Nd2fD0th1TBAsqBw=
|
||||||
github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA=
|
|
||||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
|
||||||
github.com/mdlayher/raw v0.0.0-20190303161257-764d452d77af h1:20h/EjkLGn9mV5nX9MFnGhbbeEhIGnOKPShJfBtVkVQ=
|
github.com/mdlayher/raw v0.0.0-20190303161257-764d452d77af h1:20h/EjkLGn9mV5nX9MFnGhbbeEhIGnOKPShJfBtVkVQ=
|
||||||
github.com/mdlayher/raw v0.0.0-20190303161257-764d452d77af/go.mod h1:rC/yE65s/DoHB6BzVOUBNYBGTg772JVytyAytffIZkY=
|
github.com/mdlayher/raw v0.0.0-20190303161257-764d452d77af/go.mod h1:rC/yE65s/DoHB6BzVOUBNYBGTg772JVytyAytffIZkY=
|
||||||
github.com/mdlayher/watchdog v0.0.0-20201005150459-8bdc4f41966b h1:7tUBfsEEBWfFeHOB7CUfoOamak+Gx/BlirfXyPk1WjI=
|
|
||||||
github.com/mdlayher/watchdog v0.0.0-20201005150459-8bdc4f41966b/go.mod h1:bmoJUS6qOA3uKFvF3KVuhf7mU1KQirzQMeHXtPyKEqg=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/rtr7/dhcp4 v0.0.0-20181120124042-778e8c2e24a5 h1:/kzTBQ20DbbhSNaBXiFEk2gPrGhY26kajwC1ro/Vlh8=
|
github.com/rtr7/dhcp4 v0.0.0-20181120124042-778e8c2e24a5 h1:/kzTBQ20DbbhSNaBXiFEk2gPrGhY26kajwC1ro/Vlh8=
|
||||||
@ -22,13 +23,15 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
|||||||
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
|
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
|
||||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 h1:ULYEB3JvPRE/IfO+9uO7vKV/xzVTO7XPAwm8xbf4w2g=
|
||||||
|
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200406155108-e3b113bbe6a4 h1:c1Sgqkh8v6ZxafNGG64r8C8UisIW2TKMJN8P86tKjr0=
|
golang.org/x/sys v0.0.0-20200406155108-e3b113bbe6a4 h1:c1Sgqkh8v6ZxafNGG64r8C8UisIW2TKMJN8P86tKjr0=
|
||||||
golang.org/x/sys v0.0.0-20200406155108-e3b113bbe6a4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200406155108-e3b113bbe6a4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201005065044-765f4ea38db3 h1:9Dt0vhJUQR70NuYBi/EFF+uAOebN40T4F1PZ7PKYrdw=
|
|
||||||
golang.org/x/sys v0.0.0-20201005065044-765f4ea38db3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
102
gokrazy.go
102
gokrazy.go
@ -16,12 +16,10 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mdlayher/watchdog"
|
|
||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
|
|
||||||
"github.com/gokrazy/gokrazy/internal/iface"
|
"github.com/gokrazy/gokrazy/internal/iface"
|
||||||
@ -53,28 +51,17 @@ func configureLoopback() error {
|
|||||||
return cs.SetNetmask(net.IPMask([]byte{255, 0, 0, 0}))
|
return cs.SetNetmask(net.IPMask([]byte{255, 0, 0, 0}))
|
||||||
}
|
}
|
||||||
|
|
||||||
// runWatchdog periodically pings the hardware watchdog.
|
// watchdog periodically pings the hardware watchdog.
|
||||||
func runWatchdog() {
|
func watchdog() {
|
||||||
d, err := watchdog.Open()
|
f, err := os.OpenFile("/dev/watchdog", os.O_WRONLY, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("disabling hardware watchdog, as it could not be opened: %v", err)
|
log.Printf("disabling hardware watchdog, as it could not be opened: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer d.Close()
|
defer f.Close()
|
||||||
|
|
||||||
var timeout string
|
|
||||||
if t, err := d.Timeout(); err != nil {
|
|
||||||
// Assume the device cannot report the watchdog timeout.
|
|
||||||
timeout = "unknown"
|
|
||||||
} else {
|
|
||||||
timeout = t.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("found hardware watchdog %q with timeout %s, pinging...", d.Identity, timeout)
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
if err := d.Ping(); err != nil {
|
if _, _, errno := unix.Syscall(unix.SYS_IOCTL, f.Fd(), unix.WDIOC_KEEPALIVE, 0); errno != 0 {
|
||||||
log.Printf("hardware watchdog ping failed: %v", err)
|
log.Printf("hardware watchdog ping failed: %v", errno)
|
||||||
}
|
}
|
||||||
time.Sleep(1 * time.Second)
|
time.Sleep(1 * time.Second)
|
||||||
}
|
}
|
||||||
@ -105,37 +92,6 @@ func setupTLS() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// readConfigFile reads configuration files from /perm /etc or / and returns trimmed content as string
|
|
||||||
func readConfigFile(fileName string) (string, error) {
|
|
||||||
str, err := ioutil.ReadFile("/perm/" + fileName)
|
|
||||||
if err != nil {
|
|
||||||
str, err = ioutil.ReadFile("/etc/" + fileName)
|
|
||||||
}
|
|
||||||
if err != nil && os.IsNotExist(err) {
|
|
||||||
str, err = ioutil.ReadFile("/" + fileName)
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.TrimSpace(string(str)), err
|
|
||||||
}
|
|
||||||
|
|
||||||
// readPortFromConfigFile reads port from config file
|
|
||||||
func readPortFromConfigFile(fileName, defaultPort string) string {
|
|
||||||
port, err := readConfigFile(fileName)
|
|
||||||
if err != nil {
|
|
||||||
if !os.IsNotExist(err) {
|
|
||||||
log.Printf("reading %s failed: %v", fileName, err)
|
|
||||||
}
|
|
||||||
return defaultPort
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := strconv.Atoi(port); err != nil {
|
|
||||||
log.Printf("invalid port in %s: %v", fileName, err)
|
|
||||||
return defaultPort
|
|
||||||
}
|
|
||||||
|
|
||||||
return port
|
|
||||||
}
|
|
||||||
|
|
||||||
// Boot configures basic system settings. More specifically, it:
|
// Boot configures basic system settings. More specifically, it:
|
||||||
//
|
//
|
||||||
// - mounts /dev, /tmp, /proc, /sys and /perm file systems
|
// - mounts /dev, /tmp, /proc, /sys and /perm file systems
|
||||||
@ -151,7 +107,7 @@ func readPortFromConfigFile(fileName, defaultPort string) string {
|
|||||||
// userBuildTimestamp will be exposed on the HTTP status handlers that
|
// userBuildTimestamp will be exposed on the HTTP status handlers that
|
||||||
// are set up by Supervise.
|
// are set up by Supervise.
|
||||||
func Boot(userBuildTimestamp string) error {
|
func Boot(userBuildTimestamp string) error {
|
||||||
go runWatchdog()
|
go watchdog()
|
||||||
|
|
||||||
buildTimestamp = userBuildTimestamp
|
buildTimestamp = userBuildTimestamp
|
||||||
|
|
||||||
@ -171,11 +127,18 @@ func Boot(userBuildTimestamp string) error {
|
|||||||
}
|
}
|
||||||
hostname = string(hostnameb)
|
hostname = string(hostnameb)
|
||||||
|
|
||||||
pw, err := readConfigFile("gokr-pw.txt")
|
pw, err := ioutil.ReadFile("/perm/gokr-pw.txt")
|
||||||
|
if err != nil {
|
||||||
|
pw, err = ioutil.ReadFile("/etc/gokr-pw.txt")
|
||||||
|
}
|
||||||
|
if err != nil && os.IsNotExist(err) {
|
||||||
|
pw, err = ioutil.ReadFile("/gokr-pw.txt")
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("could read neither /perm/gokr-pw.txt, nor /etc/gokr-pw.txt, nor /gokr-pw.txt: %v", err)
|
return fmt.Errorf("could read neither /perm/gokr-pw.txt, nor /etc/gokr-pw.txt, nor /gokr-pw.txt: %v", err)
|
||||||
}
|
}
|
||||||
httpPassword = pw
|
|
||||||
|
httpPassword = strings.TrimSpace(string(pw))
|
||||||
|
|
||||||
if err := configureLoopback(); err != nil {
|
if err := configureLoopback(); err != nil {
|
||||||
return err
|
return err
|
||||||
@ -190,11 +153,11 @@ func Boot(userBuildTimestamp string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func updateListenerPairs(httpPort, httpsPort string, useTLS bool, tlsConfig *tls.Config) error {
|
func updateListenerPairs(httpPort, httpsPort string, useTLS bool, tlsConfig *tls.Config) error {
|
||||||
if err := updateListeners(httpPort, httpsPort, useTLS, nil); err != nil {
|
if err := updateListeners(httpPort, useTLS, nil); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if useTLS {
|
if useTLS {
|
||||||
if err := updateListeners(httpsPort, "", useTLS, tlsConfig); err != nil {
|
if err := updateListeners(httpsPort, useTLS, tlsConfig); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -226,8 +189,6 @@ func tryStartShell() error {
|
|||||||
return lastErr
|
return lastErr
|
||||||
}
|
}
|
||||||
|
|
||||||
var add []*service
|
|
||||||
|
|
||||||
// Supervise continuously restarts the processes specified in commands
|
// Supervise continuously restarts the processes specified in commands
|
||||||
// unless they run DontStartOnBoot.
|
// unless they run DontStartOnBoot.
|
||||||
//
|
//
|
||||||
@ -241,9 +202,6 @@ func Supervise(commands []*exec.Cmd) error {
|
|||||||
services := make([]*service, len(commands))
|
services := make([]*service, len(commands))
|
||||||
for idx, cmd := range commands {
|
for idx, cmd := range commands {
|
||||||
services[idx] = &service{cmd: cmd}
|
services[idx] = &service{cmd: cmd}
|
||||||
if cmd.Path == "/user/backupd" {
|
|
||||||
add = append(add, services[idx])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
superviseServices(services)
|
superviseServices(services)
|
||||||
|
|
||||||
@ -253,10 +211,7 @@ func Supervise(commands []*exec.Cmd) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
httpPort := readPortFromConfigFile("http-port.txt", "80")
|
if err := updateListenerPairs("80", "443", useTLS, tlsConfig); err != nil {
|
||||||
httpsPort := readPortFromConfigFile("https-port.txt", "443")
|
|
||||||
|
|
||||||
if err := updateListenerPairs(httpPort, httpsPort, useTLS, tlsConfig); err != nil {
|
|
||||||
return fmt.Errorf("updating listeners: %v", err)
|
return fmt.Errorf("updating listeners: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -274,10 +229,7 @@ func Supervise(commands []*exec.Cmd) error {
|
|||||||
m.Header.Type != syscall.RTM_DELADDR {
|
m.Header.Type != syscall.RTM_DELADDR {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, v := range add {
|
if err := updateListenerPairs("80", "443", useTLS, tlsConfig); err != nil {
|
||||||
v.Signal(unix.SIGUSR1)
|
|
||||||
}
|
|
||||||
if err := updateListenerPairs(httpPort, httpsPort, useTLS, tlsConfig); err != nil {
|
|
||||||
log.Printf("updating listeners: %v", err)
|
log.Printf("updating listeners: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -292,22 +244,12 @@ func Supervise(commands []*exec.Cmd) error {
|
|||||||
signal.Notify(c, unix.SIGHUP)
|
signal.Notify(c, unix.SIGHUP)
|
||||||
|
|
||||||
for range c {
|
for range c {
|
||||||
if err := updateListenerPairs(httpPort, httpsPort, useTLS, tlsConfig); err != nil {
|
if err := updateListenerPairs("80", "443", useTLS, tlsConfig); err != nil {
|
||||||
log.Printf("updating listeners: %v", err)
|
log.Printf("updating listeners: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
go func() {
|
|
||||||
c := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(c, unix.SIGTERM)
|
|
||||||
|
|
||||||
for range c {
|
|
||||||
killSupervisedServices()
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
buf := make([]byte, 1)
|
buf := make([]byte, 1)
|
||||||
for {
|
for {
|
||||||
@ -318,7 +260,7 @@ func Supervise(commands []*exec.Cmd) error {
|
|||||||
|
|
||||||
if err := tryStartShell(); err != nil {
|
if err := tryStartShell(); err != nil {
|
||||||
log.Printf("could not start shell: %v", err)
|
log.Printf("could not start shell: %v", err)
|
||||||
if err := updateListenerPairs(httpPort, httpsPort, useTLS, tlsConfig); err != nil {
|
if err := updateListenerPairs("80", "443", useTLS, tlsConfig); err != nil {
|
||||||
log.Printf("updating listeners: %v", err)
|
log.Printf("updating listeners: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,24 +1,10 @@
|
|||||||
package gokrazy
|
package gokrazy
|
||||||
|
|
||||||
import (
|
import "net/http"
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
func httpsRedirect(redirectPort string) func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
host, _, _ := net.SplitHostPort(r.RemoteAddr)
|
|
||||||
ip := net.ParseIP(host)
|
|
||||||
if ip.IsLoopback() {
|
|
||||||
http.DefaultServeMux.ServeHTTP(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// TODO: Configurable https-port
|
||||||
|
func httpsRedirect(w http.ResponseWriter, r *http.Request) {
|
||||||
r.URL.Host = r.Host
|
r.URL.Host = r.Host
|
||||||
if redirectPort != "443" {
|
|
||||||
r.URL.Host += ":" + redirectPort
|
|
||||||
}
|
|
||||||
r.URL.Scheme = "https"
|
r.URL.Scheme = "https"
|
||||||
http.Redirect(w, r, r.URL.String(), http.StatusFound) // Redirect to https
|
http.Redirect(w, r, r.URL.String(), http.StatusFound) // Redirect to https
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@ -12,7 +12,7 @@ var assets = map[string][]byte{
|
|||||||
"assets/bootstrap-table-1.11.0.min.js": assets_7,
|
"assets/bootstrap-table-1.11.0.min.js": assets_7,
|
||||||
"assets/jquery-3.1.1.min.js": assets_8,
|
"assets/jquery-3.1.1.min.js": assets_8,
|
||||||
}
|
}
|
||||||
var assets_0 = []byte("<!DOCTYPE html>\n<html lang=\"en\">\n<title>{{ .Hostname }} — gokrazy</title>\n<link rel=\"stylesheet\" href=\"/bootstrap-3.3.7.min.css\" />\n<link rel=\"stylesheet\" href=\"/bootstrap-table-1.11.0.min.css\" />\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 <table class=\"navbar-text navbar-right\" style=\"border-spacing: 10px 0; border-collapse: separate\">\n <tr>\n <th>host</th>\n <td>{{ .Hostname }}</td>\n </tr>\n <tr>\n <th>kernel</th>\n <td>{{ .Kernel }}</td>\n </tr>\n {{ if (ne .Model \"\") }}\n <tr>\n <th>model</th>\n <td>{{ .Model }}</td>\n </tr>\n {{ end }}\n {{ if .EEPROM }}\n <tr>\n <th>EEPROM<br>(SHA256)</th>\n <td>{{ shortenSHA256 .EEPROM.PieepromSHA256 }}<br>{{ shortenSHA256 .EEPROM.VL805SHA256 }}</td>\n </tr>\n {{ end }}\n </table>\n\n </div><!-- /.navbar-collapse -->\n </div><!-- /.container-fluid -->\n </nav>\n\n <div class=\"container\">\n")
|
var assets_0 = []byte("<!DOCTYPE html>\n<html lang=\"en\">\n<title>{{ .Hostname }} — gokrazy</title>\n<link rel=\"stylesheet\" href=\"/bootstrap-3.3.7.min.css\" />\n<link rel=\"stylesheet\" href=\"/bootstrap-table-1.11.0.min.css\" />\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 <table class=\"navbar-text navbar-right\" style=\"border-spacing: 10px 0; border-collapse: separate\">\n <tr>\n <th>host</th>\n <td>{{ .Hostname }}</td>\n </tr>\n {{ if (ne .Model \"\") }}\n <tr>\n <th>model</th>\n <td>{{ .Model }}</td>\n </tr>\n {{ end }}\n {{ if .EEPROM }}\n <tr>\n <th>EEPROM<br>(SHA256)</th>\n <td>{{ shortenSHA256 .EEPROM.PieepromSHA256 }}<br>{{ shortenSHA256 .EEPROM.VL805SHA256 }}</td>\n </tr>\n {{ end }}\n </table>\n\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=\"/jquery-3.1.1.min.js\"></script>\n<script src=\"/bootstrap-table-1.11.0.min.js\"></script>\n\n</html>\n")
|
var assets_1 = []byte("\n</div>\n\n<script src=\"/jquery-3.1.1.min.js\"></script>\n<script src=\"/bootstrap-table-1.11.0.min.js\"></script>\n\n</html>\n")
|
||||||
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 }}\n\n{{ if ne .PARTUUID \"\" }}\nNo permanent storage mounted. To create a filesystem for permanent storage, plug the SD card into a Linux computer and use <code>mkfs.ext4 /dev/disk/by-partuuid/{{ .PARTUUID }}-04</code>.\n{{ else }}\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{{ end }}\n\n{{ else }}\n<strong>{{ .PermDev }}</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, $addr := .PrivateAddrs }}\n<li>{{ $addr }}</li>\n{{ end }}\n</ul>\n\n<h2>public network addresses</h2>\n<ul>\n{{ range $idx, $addr := .PublicAddrs }}\n<li>{{ $addr }}</li>\n{{ end }}\n</ul>\n\n\n</div>\n</div>\n\n{{ template \"footer\" . }}\n")
|
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 }}\n\n{{ if ne .PARTUUID \"\" }}\nNo permanent storage mounted. To create a filesystem for permanent storage, plug the SD card into a Linux computer and use <code>mkfs.ext4 /dev/disk/by-partuuid/{{ .PARTUUID }}-04</code>.\n{{ else }}\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{{ end }}\n\n{{ else }}\n<strong>{{ .PermDev }}</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, $addr := .PrivateAddrs }}\n<li>{{ $addr }}</li>\n{{ end }}\n</ul>\n\n<h2>public network addresses</h2>\n<ul>\n{{ range $idx, $addr := .PublicAddrs }}\n<li>{{ $addr }}</li>\n{{ end }}\n</ul>\n\n\n</div>\n</div>\n\n{{ template \"footer\" . }}\n")
|
||||||
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>\n <form method=\"POST\" action=\"/restart\">\n <input type=\"hidden\" name=\"xsrftoken\" value=\"{{ .XsrfToken }}\">\n <input type=\"hidden\" name=\"path\" value=\"{{ .Service.Name }}\">\n <input type=\"submit\" value=\"restart\">\n </form>\n <form method=\"POST\" action=\"/stop\">\n <input type=\"hidden\" name=\"xsrftoken\" value=\"{{ .XsrfToken }}\">\n <input type=\"hidden\" name=\"path\" value=\"{{ .Service.Name }}\">\n <input type=\"submit\" value=\"stop\">\n </form></td>\n</tr>\n</table>\n\n <h3>module info</h3>\n <pre>{{ .Service.ModuleInfo }}</pre>\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\" . }}\n")
|
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>\n <form method=\"POST\" action=\"/restart\">\n <input type=\"hidden\" name=\"xsrftoken\" value=\"{{ .XsrfToken }}\">\n <input type=\"hidden\" name=\"path\" value=\"{{ .Service.Name }}\">\n <input type=\"submit\" value=\"restart\">\n </form>\n <form method=\"POST\" action=\"/stop\">\n <input type=\"hidden\" name=\"xsrftoken\" value=\"{{ .XsrfToken }}\">\n <input type=\"hidden\" name=\"path\" value=\"{{ .Service.Name }}\">\n <input type=\"submit\" value=\"stop\">\n </form></td>\n</tr>\n</table>\n\n <h3>module info</h3>\n <pre>{{ .Service.ModuleInfo }}</pre>\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\" . }}\n")
|
||||||
|
@ -1,56 +0,0 @@
|
|||||||
// Copyright 2018 Google Inc.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
// Package notify implements sending signals (such as SIGUSR1) to processes.
|
|
||||||
package notify
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"regexp"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
var numericRe = regexp.MustCompile(`^[0-9]+$`)
|
|
||||||
|
|
||||||
func Process(name string, sig os.Signal) error {
|
|
||||||
fis, err := ioutil.ReadDir("/proc")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, fi := range fis {
|
|
||||||
if !fi.IsDir() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !numericRe.MatchString(fi.Name()) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
b, err := ioutil.ReadFile(filepath.Join("/proc", fi.Name(), "cmdline"))
|
|
||||||
if err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
continue // process vanished
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !strings.Contains(string(b), name) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
pid, _ := strconv.Atoi(fi.Name()) // already verified to be numeric
|
|
||||||
p, _ := os.FindProcess(pid)
|
|
||||||
return p.Signal(sig)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -141,7 +141,7 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// tlsConfig: tlsConfig. nil, if the listeners should not use https (e.g. for redirects)
|
// tlsConfig: tlsConfig. nil, if the listeners should not use https (e.g. for redirects)
|
||||||
func updateListeners(port, redirectPort string, tlsEnabled bool, tlsConfig *tls.Config) error {
|
func updateListeners(port string, tlsEnabled bool, tlsConfig *tls.Config) error {
|
||||||
hosts, err := PrivateInterfaceAddrs()
|
hosts, err := PrivateInterfaceAddrs()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -180,7 +180,7 @@ func updateListeners(port, redirectPort string, tlsEnabled bool, tlsConfig *tls.
|
|||||||
if tlsEnabled && tlsConfig == nil {
|
if tlsEnabled && tlsConfig == nil {
|
||||||
// "Redirect" server
|
// "Redirect" server
|
||||||
srv = &http.Server{
|
srv = &http.Server{
|
||||||
Handler: http.HandlerFunc(httpsRedirect(redirectPort)),
|
Handler: http.HandlerFunc(httpsRedirect),
|
||||||
TLSConfig: tlsConfig,
|
TLSConfig: tlsConfig,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
8
mount.go
8
mount.go
@ -95,12 +95,8 @@ func mountfs() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dev := rootdev.Partition(rootdev.Perm)
|
dev := rootdev.Partition(rootdev.Perm)
|
||||||
for _, fstype := range []string{"ext4", "vfat"} {
|
if err := syscall.Mount(dev, "/perm", "ext4", 0, ""); err != nil {
|
||||||
if err := syscall.Mount(dev, "/perm", fstype, 0, ""); err != nil {
|
log.Printf("Could not mount permanent storage partition %s: %v", dev, err)
|
||||||
log.Printf("Could not mount permanent storage partition %s as %s: %v", dev, fstype, err)
|
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
25
status.go
25
status.go
@ -151,21 +151,6 @@ func lastInstalledEepromVersion() (*eepromVersion, error) {
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseUtsname(u unix.Utsname) string {
|
|
||||||
if u == (unix.Utsname{}) {
|
|
||||||
// Empty utsname, no info to parse.
|
|
||||||
return "unknown"
|
|
||||||
}
|
|
||||||
|
|
||||||
str := func(b [65]byte) string {
|
|
||||||
// Trim all trailing NULL bytes.
|
|
||||||
return string(bytes.TrimRight(b[:], "\x00"))
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("%s %s (%s)",
|
|
||||||
str(u.Sysname), str(u.Release), str(u.Machine))
|
|
||||||
}
|
|
||||||
|
|
||||||
func initStatus(services []*service) {
|
func initStatus(services []*service) {
|
||||||
model := Model()
|
model := Model()
|
||||||
|
|
||||||
@ -174,12 +159,6 @@ func initStatus(services []*service) {
|
|||||||
log.Printf("getting EEPROM version: %v", err)
|
log.Printf("getting EEPROM version: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var uname unix.Utsname
|
|
||||||
if err := unix.Uname(&uname); err != nil {
|
|
||||||
log.Printf("getting uname: %v", err)
|
|
||||||
}
|
|
||||||
kernel := parseUtsname(uname)
|
|
||||||
|
|
||||||
commonTmpls := template.New("root").Funcs(map[string]interface{}{
|
commonTmpls := template.New("root").Funcs(map[string]interface{}{
|
||||||
"shortenSHA256": func(hash string) string {
|
"shortenSHA256": func(hash string) string {
|
||||||
if len(hash) > 10 {
|
if len(hash) > 10 {
|
||||||
@ -272,7 +251,6 @@ func initStatus(services []*service) {
|
|||||||
Model string
|
Model string
|
||||||
XsrfToken int32
|
XsrfToken int32
|
||||||
EEPROM *eepromVersion
|
EEPROM *eepromVersion
|
||||||
Kernel string
|
|
||||||
}{
|
}{
|
||||||
Service: svc,
|
Service: svc,
|
||||||
BuildTimestamp: buildTimestamp,
|
BuildTimestamp: buildTimestamp,
|
||||||
@ -280,7 +258,6 @@ func initStatus(services []*service) {
|
|||||||
Model: model,
|
Model: model,
|
||||||
XsrfToken: token,
|
XsrfToken: token,
|
||||||
EEPROM: lastInstalledEepromVersion,
|
EEPROM: lastInstalledEepromVersion,
|
||||||
Kernel: kernel,
|
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
@ -324,7 +301,6 @@ func initStatus(services []*service) {
|
|||||||
Model string
|
Model string
|
||||||
PARTUUID string
|
PARTUUID string
|
||||||
EEPROM *eepromVersion
|
EEPROM *eepromVersion
|
||||||
Kernel string
|
|
||||||
}{
|
}{
|
||||||
Services: services,
|
Services: services,
|
||||||
PermDev: rootdev.Partition(rootdev.Perm),
|
PermDev: rootdev.Partition(rootdev.Perm),
|
||||||
@ -339,7 +315,6 @@ func initStatus(services []*service) {
|
|||||||
Model: model,
|
Model: model,
|
||||||
PARTUUID: rootdev.PARTUUID(),
|
PARTUUID: rootdev.PARTUUID(),
|
||||||
EEPROM: lastInstalledEepromVersion,
|
EEPROM: lastInstalledEepromVersion,
|
||||||
Kernel: kernel,
|
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
|
11
supervise.go
11
supervise.go
@ -75,11 +75,8 @@ func (w *remoteSyslogWriter) Write(b []byte) (int, error) {
|
|||||||
w.lines.Write(b)
|
w.lines.Write(b)
|
||||||
w.syslogMu.Lock()
|
w.syslogMu.Lock()
|
||||||
defer w.syslogMu.Unlock()
|
defer w.syslogMu.Unlock()
|
||||||
if w.syslog == nil {
|
if w.syslog != nil {
|
||||||
return len(b), nil
|
w.syslog.Write(b)
|
||||||
}
|
|
||||||
for _, line := range strings.Split(strings.TrimSpace(string(b)), "\n") {
|
|
||||||
w.syslog.Write([]byte(line + "\n"))
|
|
||||||
}
|
}
|
||||||
return len(b), nil
|
return len(b), nil
|
||||||
}
|
}
|
||||||
@ -303,10 +300,6 @@ func supervise(s *service) {
|
|||||||
// just work.
|
// just work.
|
||||||
cmd.Env = append(cmd.Env, "HOME=/perm/"+filepath.Base(s.cmd.Path))
|
cmd.Env = append(cmd.Env, "HOME=/perm/"+filepath.Base(s.cmd.Path))
|
||||||
|
|
||||||
if err := os.MkdirAll("/perm/"+filepath.Base(s.cmd.Path), 0755); err == nil {
|
|
||||||
cmd.Dir = "/perm/" + filepath.Base(s.cmd.Path)
|
|
||||||
}
|
|
||||||
|
|
||||||
attempt++
|
attempt++
|
||||||
|
|
||||||
if err := cmd.Start(); err != nil {
|
if err := cmd.Start(); err != nil {
|
||||||
|
16
update.go
16
update.go
@ -11,6 +11,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
@ -19,6 +20,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gokrazy/internal/fat"
|
"github.com/gokrazy/internal/fat"
|
||||||
"github.com/gokrazy/internal/rootdev"
|
"github.com/gokrazy/internal/rootdev"
|
||||||
|
"github.com/klauspost/compress/zstd"
|
||||||
)
|
)
|
||||||
|
|
||||||
var rootRe = regexp.MustCompile(`root=[^ ]+`)
|
var rootRe = regexp.MustCompile(`root=[^ ]+`)
|
||||||
@ -100,7 +102,17 @@ func nonConcurrentUpdateHandler(dest string) func(http.ResponseWriter, *http.Req
|
|||||||
default:
|
default:
|
||||||
hash = sha256.New()
|
hash = sha256.New()
|
||||||
}
|
}
|
||||||
if err := streamRequestTo(dest, io.TeeReader(r.Body, hash)); err != nil {
|
rd := io.Reader(r.Body)
|
||||||
|
if strings.EqualFold(r.Header.Get("Content-Encoding"), "zstd") {
|
||||||
|
dec, err := zstd.NewReader(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("updating %q failed: %v", dest, err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rd = dec
|
||||||
|
}
|
||||||
|
if err := streamRequestTo(dest, io.TeeReader(rd, hash)); err != nil {
|
||||||
log.Printf("updating %q failed: %v", dest, err)
|
log.Printf("updating %q failed: %v", dest, err)
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
@ -133,7 +145,7 @@ func initUpdate() error {
|
|||||||
// feature support (e.g. PARTUUID= support) between the packer and update
|
// feature support (e.g. PARTUUID= support) between the packer and update
|
||||||
// target.
|
// target.
|
||||||
http.HandleFunc("/update/features", func(w http.ResponseWriter, r *http.Request) {
|
http.HandleFunc("/update/features", func(w http.ResponseWriter, r *http.Request) {
|
||||||
fmt.Fprintf(w, "partuuid,updatehash,")
|
fmt.Fprintf(w, "partuuid,updatehash,zstd,")
|
||||||
})
|
})
|
||||||
http.HandleFunc("/update/mbr", nonConcurrentUpdateHandler(rootdev.BlockDevice()))
|
http.HandleFunc("/update/mbr", nonConcurrentUpdateHandler(rootdev.BlockDevice()))
|
||||||
http.HandleFunc("/update/root", nonConcurrentUpdateHandler(rootdev.Partition(rootdev.InactiveRootPartition())))
|
http.HandleFunc("/update/root", nonConcurrentUpdateHandler(rootdev.Partition(rootdev.InactiveRootPartition())))
|
||||||
|
@ -48,11 +48,6 @@ Drive.
|
|||||||
beatbox is a Raspberry Pi 3-based toy that combines a Mir:ror and NFC
|
beatbox is a Raspberry Pi 3-based toy that combines a Mir:ror and NFC
|
||||||
figurines for playing music stored on the device or directly from Spotify.
|
figurines for playing music stored on the device or directly from Spotify.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
|
||||||
<a href="https://github.com/mdlayher/consrv"><strong>consrv</strong></a><br>
|
|
||||||
consrv is a Raspberry Pi 4-based appliance that provides a basic SSH to
|
|
||||||
serial console bridge for accessing remote devices.
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user