Compare commits

...

11 Commits

Author SHA1 Message Date
Elara e9d7cf1770 Update drpc library version 2023-03-28 13:27:14 -07:00
Elara 1f15d0ae51 Remove replace directive for drpc 2023-03-28 13:25:26 -07:00
Elara 6113ac019e Gracefully shut down each component before exiting 2023-03-26 14:34:29 -07:00
Elara 947ab7fbcb Use EINVAL for Invalid Offset error in FUSE 2023-03-26 13:01:25 -07:00
Elara 948678790a Use type switch for syscallErr 2023-03-25 16:03:56 -07:00
Elara 8fb430a359 Set some log levels to Debug 2023-03-25 15:53:46 -07:00
Elara 6ac4ab9f4d Removed unreachable code 2023-03-25 15:52:46 -07:00
Elara f25a893475 Create new type for node kinds 2023-03-25 15:52:13 -07:00
Elara 0defa1ce91 Mention FUSE support in README 2023-03-25 15:27:28 -07:00
Elara ee4f563b05 Run formatter 2023-03-25 15:24:46 -07:00
yannickulrich 9ecd45dadd Added FUSE support (#55)
This exposes the watches' file system over FUSE. This way, we can access files on the watch without having to go through `itctl` or developing 3rd party tools.

**Features**

- [x] read/write access to the file system
- [x] read access to momentary sensor data
- [x] live access to sensor data (i.e. WatchMotion rather than Motion)
- [x] configuration of mount point

Co-authored-by: Yannick Ulrich <yannick.ulrich@durham.ac.uk>
Reviewed-on: https://gitea.arsenm.dev/Arsen6331/itd/pulls/55
Co-authored-by: yannickulrich <yannick.ulrich@protonmail.com>
Co-committed-by: yannickulrich <yannick.ulrich@protonmail.com>
2023-03-25 22:23:51 +00:00
17 changed files with 1003 additions and 132 deletions

View File

@ -22,6 +22,7 @@
- Weather
- BLE Filesystem
- Navigation (PureMaps)
- FUSE Filesystem
---
@ -197,4 +198,4 @@ Most of the time, the daemon does not need to be restarted for config changes to
Location data from OpenStreetMap Nominatim, &copy; [OpenStreetMap](https://www.openstreetmap.org/copyright) contributors
Weather data from the [Norwegian Meteorological Institute](https://www.met.no/en)
Weather data from the [Norwegian Meteorological Institute](https://www.met.no/en)

View File

@ -10,7 +10,7 @@ import (
"go.arsenm.dev/logger/log"
)
func initCallNotifs(ctx context.Context, dev *infinitime.Device) error {
func initCallNotifs(ctx context.Context, wg WaitGroup, dev *infinitime.Device) error {
// Connect to system bus. This connection is for method calls.
conn, err := utils.NewSystemBusConn(ctx)
if err != nil {
@ -53,49 +53,55 @@ func initCallNotifs(ctx context.Context, dev *infinitime.Device) error {
var respHandlerOnce sync.Once
var callObj dbus.BusObject
wg.Add(1)
go func() {
// For every message received
for event := range callCh {
// Get path to call object
callPath := event.Body[0].(dbus.ObjectPath)
// Get call object
callObj = conn.Object("org.freedesktop.ModemManager1", callPath)
defer wg.Done("callNotifs")
for {
select {
case event := <-callCh:
// Get path to call object
callPath := event.Body[0].(dbus.ObjectPath)
// Get call object
callObj = conn.Object("org.freedesktop.ModemManager1", callPath)
// Get phone number from call object using method call connection
phoneNum, err := getPhoneNum(conn, callObj)
if err != nil {
log.Error("Error getting phone number").Err(err).Send()
continue
}
// Send call notification to InfiniTime
resCh, err := dev.NotifyCall(phoneNum)
if err != nil {
continue
}
go respHandlerOnce.Do(func() {
// Wait for PineTime response
for res := range resCh {
switch res {
case infinitime.CallStatusAccepted:
// Attempt to accept call
err = acceptCall(ctx, conn, callObj)
if err != nil {
log.Warn("Error accepting call").Err(err).Send()
}
case infinitime.CallStatusDeclined:
// Attempt to decline call
err = declineCall(ctx, conn, callObj)
if err != nil {
log.Warn("Error declining call").Err(err).Send()
}
case infinitime.CallStatusMuted:
// Warn about unimplemented muting
log.Warn("Muting calls is not implemented").Send()
}
// Get phone number from call object using method call connection
phoneNum, err := getPhoneNum(conn, callObj)
if err != nil {
log.Error("Error getting phone number").Err(err).Send()
continue
}
})
// Send call notification to InfiniTime
resCh, err := dev.NotifyCall(phoneNum)
if err != nil {
continue
}
go respHandlerOnce.Do(func() {
// Wait for PineTime response
for res := range resCh {
switch res {
case infinitime.CallStatusAccepted:
// Attempt to accept call
err = acceptCall(ctx, conn, callObj)
if err != nil {
log.Warn("Error accepting call").Err(err).Send()
}
case infinitime.CallStatusDeclined:
// Attempt to decline call
err = declineCall(ctx, conn, callObj)
if err != nil {
log.Warn("Error declining call").Err(err).Send()
}
case infinitime.CallStatusMuted:
// Warn about unimplemented muting
log.Warn("Muting calls is not implemented").Send()
}
}
})
case <-ctx.Done():
return
}
}
}()

View File

@ -106,5 +106,8 @@ func setCfgDefaults() {
"notifs.ignore.body": []string{},
"music.vol.interval": 5,
"fuse.enabled": false,
"fuse.mountpoint": "/tmp/itd/mnt",
}, "."), nil)
}

66
fuse.go Normal file
View File

@ -0,0 +1,66 @@
package main
import (
"context"
"os"
"github.com/hanwen/go-fuse/v2/fs"
"github.com/hanwen/go-fuse/v2/fuse"
"go.arsenm.dev/infinitime"
"go.arsenm.dev/itd/internal/fusefs"
"go.arsenm.dev/logger/log"
)
func startFUSE(ctx context.Context, wg WaitGroup, dev *infinitime.Device) error {
// This is where we'll mount the FS
err := os.MkdirAll(k.String("fuse.mountpoint"), 0o755)
if err != nil && !os.IsExist(err) {
return err
}
// Ignore the error because nothing might be mounted on the mountpoint
_ = fusefs.Unmount(k.String("fuse.mountpoint"))
root, err := fusefs.BuildRootNode(dev)
if err != nil {
log.Error("Building root node failed").
Err(err).
Send()
return err
}
server, err := fs.Mount(k.String("fuse.mountpoint"), root, &fs.Options{
MountOptions: fuse.MountOptions{
// Set to true to see how the file system works.
Debug: false,
SingleThreaded: true,
},
})
if err != nil {
log.Error("Mounting failed").
Str("target", k.String("fuse.mountpoint")).
Err(err).
Send()
return err
}
log.Info("Mounted on target").
Str("target", k.String("fuse.mountpoint")).
Send()
fusefs.BuildProperties(dev)
if err != nil {
log.Warn("Error getting BLE filesystem").Err(err).Send()
return err
}
wg.Add(1)
go func() {
defer wg.Done("fuse")
<-ctx.Done()
server.Unmount()
}()
return nil
}

5
go.mod
View File

@ -10,11 +10,12 @@ require (
github.com/cheggaaa/pb/v3 v3.1.0
github.com/gen2brain/dlgs v0.0.0-20220603100644-40c77870fa8d
github.com/godbus/dbus/v5 v5.1.0
github.com/hanwen/go-fuse/v2 v2.2.0
github.com/knadh/koanf v1.4.4
github.com/mattn/go-isatty v0.0.17
github.com/mozillazg/go-pinyin v0.19.0
github.com/urfave/cli/v2 v2.23.7
go.arsenm.dev/drpc v0.0.0-20230104221210-aa7525743d98
go.arsenm.dev/drpc v0.0.0-20230328202554-c1f2aa71e794
go.arsenm.dev/infinitime v0.0.0-20230104230015-512d48bc2469
go.arsenm.dev/logger v0.0.0-20230104225304-d706171ea6df
golang.org/x/text v0.5.0
@ -73,7 +74,7 @@ require (
golang.org/x/mobile v0.0.0-20221110043201-43a038452099 // indirect
golang.org/x/mod v0.7.0 // indirect
golang.org/x/net v0.4.0 // indirect
golang.org/x/sys v0.3.0 // indirect
golang.org/x/sys v0.6.0 // indirect
golang.org/x/tools v0.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
honnef.co/go/js/dom v0.0.0-20221001195520-26252dedbe70 // indirect

12
go.sum
View File

@ -257,6 +257,8 @@ github.com/goxjs/gl v0.0.0-20210104184919-e3fafc6f8f2a/go.mod h1:dy/f2gjY09hwVfI
github.com/goxjs/glfw v0.0.0-20191126052801-d2efb5f20838/go.mod h1:oS8P8gVOT4ywTcjV6wZlOU4GuVFQ8F5328KY3MJ79CY=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hanwen/go-fuse/v2 v2.2.0 h1:jo5QZYmBLNcl9ovypWaQ5yXMSSV+Ch68xoC3rtZvvBM=
github.com/hanwen/go-fuse/v2 v2.2.0/go.mod h1:B1nGE/6RBFyBRC1RRnf23UpwCdyJ31eukw34oAKukAc=
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
github.com/hashicorp/consul/api v1.13.0/go.mod h1:ZlVrynguJKcYr54zGaDbaL3fOvKC9m72FhPvA8T35KQ=
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
@ -339,6 +341,8 @@ github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4=
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
github.com/lucor/goinfo v0.0.0-20210802170112-c078a2b0f08b/go.mod h1:PRq09yoB+Q2OJReAmwzKivcYyremnibWGbK7WfftHzc=
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
@ -525,8 +529,8 @@ github.com/yuin/goldmark v1.5.3/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5ta
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
github.com/zeebo/errs v1.3.0 h1:hmiaKqgYZzcVgRL1Vkc1Mn2914BbzB0IBxs+ebeutGs=
github.com/zeebo/errs v1.3.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
go.arsenm.dev/drpc v0.0.0-20230104221210-aa7525743d98 h1:Tt/05IEOeIEfovj8YLxDaZz+SP8eR1uPLtcgD5Pf4EI=
go.arsenm.dev/drpc v0.0.0-20230104221210-aa7525743d98/go.mod h1:K5cFls42m5q1RIphTVojRdXLaoCknq/kBqQt8Ow3XuA=
go.arsenm.dev/drpc v0.0.0-20230328202554-c1f2aa71e794 h1:8KQpRoQmCTgDvyHFStaIiz5NUNNqMHqVlZoGvIk+OwQ=
go.arsenm.dev/drpc v0.0.0-20230328202554-c1f2aa71e794/go.mod h1:K5cFls42m5q1RIphTVojRdXLaoCknq/kBqQt8Ow3XuA=
go.arsenm.dev/infinitime v0.0.0-20230104230015-512d48bc2469 h1:LsJHg+8rQSYnTE1sSCjBCACxUUVMZIOQani8J6wF2/E=
go.arsenm.dev/infinitime v0.0.0-20230104230015-512d48bc2469/go.mod h1:scUyDmLmCHn6CanGbau8yjTjzyhUbLJcsjmDCCKMIII=
go.arsenm.dev/logger v0.0.0-20230104225304-d706171ea6df h1:8mBHvEe7BJmpOeKSMA5YLqrGo9dCpePocTeR0C1+/2w=
@ -752,8 +756,8 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

607
internal/fusefs/fuse.go Normal file
View File

@ -0,0 +1,607 @@
package fusefs
import (
"bytes"
"context"
"io"
"strconv"
"syscall"
"github.com/hanwen/go-fuse/v2/fs"
"github.com/hanwen/go-fuse/v2/fuse"
"go.arsenm.dev/infinitime"
"go.arsenm.dev/infinitime/blefs"
"go.arsenm.dev/logger/log"
)
type ITProperty struct {
name string
Ino uint64
gen func() ([]byte, error)
}
type DirEntry struct {
isDir bool
modtime uint64
size uint32
path string
}
type ITNode struct {
fs.Inode
kind nodeKind
Ino uint64
lst []DirEntry
self DirEntry
path string
}
type nodeKind uint8
const (
nodeKindRoot = iota
nodeKindInfo
nodeKindFS
nodeKindReadOnly
)
var (
myfs *blefs.FS = nil
inodemap map[string]uint64 = nil
)
func BuildRootNode(dev *infinitime.Device) (*ITNode, error) {
var err error
inodemap = make(map[string]uint64)
myfs, err = dev.FS()
if err != nil {
log.Error("FUSE Failed to get filesystem").Err(err).Send()
return nil, err
}
return &ITNode{kind: nodeKindRoot}, nil
}
var properties = make([]ITProperty, 6)
func BuildProperties(dev *infinitime.Device) {
properties[0] = ITProperty{
"heartrate", 2,
func() ([]byte, error) {
ans, err := dev.HeartRate()
return []byte(strconv.Itoa(int(ans)) + "\n"), err
},
}
properties[1] = ITProperty{
"battery", 3,
func() ([]byte, error) {
ans, err := dev.BatteryLevel()
return []byte(strconv.Itoa(int(ans)) + "\n"), err
},
}
properties[2] = ITProperty{
"motion", 4,
func() ([]byte, error) {
ans, err := dev.Motion()
return []byte(strconv.Itoa(int(ans.X)) + " " + strconv.Itoa(int(ans.Y)) + " " + strconv.Itoa(int(ans.Z)) + "\n"), err
},
}
properties[3] = ITProperty{
"stepcount", 6,
func() ([]byte, error) {
ans, err := dev.StepCount()
return []byte(strconv.Itoa(int(ans)) + "\n"), err
},
}
properties[4] = ITProperty{
"version", 7,
func() ([]byte, error) {
ans, err := dev.Version()
return []byte(ans + "\n"), err
},
}
properties[5] = ITProperty{
"address", 8,
func() ([]byte, error) {
ans := dev.Address()
return []byte(ans + "\n"), nil
},
}
}
var _ fs.NodeReaddirer = (*ITNode)(nil)
// Readdir is part of the NodeReaddirer interface
func (n *ITNode) Readdir(ctx context.Context) (fs.DirStream, syscall.Errno) {
switch n.kind {
case 0:
// root folder
r := make([]fuse.DirEntry, 2)
r[0] = fuse.DirEntry{
Name: "info",
Ino: 0,
Mode: fuse.S_IFDIR,
}
r[1] = fuse.DirEntry{
Name: "fs",
Ino: 1,
Mode: fuse.S_IFDIR,
}
return fs.NewListDirStream(r), 0
case 1:
// info folder
r := make([]fuse.DirEntry, 6)
for ind, value := range properties {
r[ind] = fuse.DirEntry{
Name: value.name,
Ino: value.Ino,
Mode: fuse.S_IFREG,
}
}
return fs.NewListDirStream(r), 0
case 2:
// on info
files, err := myfs.ReadDir(n.path)
if err != nil {
log.Error("FUSE ReadDir failed").Str("path", n.path).Err(err).Send()
return nil, syscallErr(err)
}
log.Debug("FUSE ReadDir succeeded").Str("path", n.path).Int("objects", len(files)).Send()
r := make([]fuse.DirEntry, len(files))
n.lst = make([]DirEntry, len(files))
for ind, entry := range files {
info, err := entry.Info()
if err != nil {
log.Error("FUSE Info failed").Str("path", n.path).Err(err).Send()
return nil, syscallErr(err)
}
name := info.Name()
file := DirEntry{
path: n.path + "/" + name,
size: uint32(info.Size()),
modtime: uint64(info.ModTime().Unix()),
isDir: info.IsDir(),
}
n.lst[ind] = file
ino := inodemap[file.path]
if ino == 0 {
ino = uint64(len(inodemap)) + 1
inodemap[file.path] = ino
}
if file.isDir {
r[ind] = fuse.DirEntry{
Name: name,
Mode: fuse.S_IFDIR,
Ino: ino + 10,
}
} else {
r[ind] = fuse.DirEntry{
Name: name,
Mode: fuse.S_IFREG,
Ino: ino + 10,
}
}
}
return fs.NewListDirStream(r), 0
}
r := make([]fuse.DirEntry, 0)
return fs.NewListDirStream(r), 0
}
var _ fs.NodeLookuper = (*ITNode)(nil)
func (n *ITNode) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) {
switch n.kind {
case 0:
// root folder
if name == "info" {
stable := fs.StableAttr{
Mode: fuse.S_IFDIR,
Ino: uint64(0),
}
operations := &ITNode{kind: nodeKindInfo, Ino: 0}
child := n.NewInode(ctx, operations, stable)
return child, 0
} else if name == "fs" {
stable := fs.StableAttr{
Mode: fuse.S_IFDIR,
Ino: uint64(1),
}
operations := &ITNode{kind: nodeKindFS, Ino: 1, path: ""}
child := n.NewInode(ctx, operations, stable)
return child, 0
}
case 1:
// info folder
for _, value := range properties {
if value.name == name {
stable := fs.StableAttr{
Mode: fuse.S_IFREG,
Ino: uint64(value.Ino),
}
operations := &ITNode{kind: nodeKindReadOnly, Ino: value.Ino}
child := n.NewInode(ctx, operations, stable)
return child, 0
}
}
case 2:
// FS object
if len(n.lst) == 0 {
n.Readdir(ctx)
}
for _, file := range n.lst {
if file.path != n.path+"/"+name {
continue
}
log.Debug("FUSE Lookup successful").Str("path", file.path).Send()
if file.isDir {
stable := fs.StableAttr{
Mode: fuse.S_IFDIR,
Ino: inodemap[file.path],
}
operations := &ITNode{kind: nodeKindFS, path: file.path}
child := n.NewInode(ctx, operations, stable)
return child, 0
} else {
stable := fs.StableAttr{
Mode: fuse.S_IFREG,
Ino: inodemap[file.path],
}
operations := &ITNode{
kind: nodeKindFS, path: file.path,
self: file,
}
child := n.NewInode(ctx, operations, stable)
return child, 0
}
}
log.Warn("FUSE Lookup failed").Str("path", n.path+"/"+name).Send()
}
return nil, syscall.ENOENT
}
type bytesFileReadHandle struct {
content []byte
}
var _ fs.FileReader = (*bytesFileReadHandle)(nil)
func (fh *bytesFileReadHandle) Read(ctx context.Context, dest []byte, off int64) (fuse.ReadResult, syscall.Errno) {
log.Debug("FUSE Executing Read").Int("size", len(fh.content)).Send()
end := off + int64(len(dest))
if end > int64(len(fh.content)) {
end = int64(len(fh.content))
}
return fuse.ReadResultData(fh.content[off:end]), 0
}
type sensorFileReadHandle struct {
content []byte
}
var _ fs.FileReader = (*sensorFileReadHandle)(nil)
func (fh *sensorFileReadHandle) Read(ctx context.Context, dest []byte, off int64) (fuse.ReadResult, syscall.Errno) {
log.Debug("FUSE Executing Read").Int("size", len(fh.content)).Send()
end := off + int64(len(dest))
if end > int64(len(fh.content)) {
end = int64(len(fh.content))
}
return fuse.ReadResultData(fh.content[off:end]), 0
}
var _ fs.FileFlusher = (*sensorFileReadHandle)(nil)
func (fh *sensorFileReadHandle) Flush(ctx context.Context) (errno syscall.Errno) {
return 0
}
type bytesFileWriteHandle struct {
content []byte
path string
}
var _ fs.FileWriter = (*bytesFileWriteHandle)(nil)
func (fh *bytesFileWriteHandle) Write(ctx context.Context, data []byte, off int64) (written uint32, errno syscall.Errno) {
log.Debug("FUSE Executing Write").Str("path", fh.path).Int("prev_size", len(fh.content)).Int("next_size", len(data)).Send()
if off != int64(len(fh.content)) {
log.Error("FUSE Write file size changed unexpectedly").Int("expect", int(off)).Int("received", len(fh.content)).Send()
return 0, syscall.ENXIO
}
fh.content = append(fh.content[:], data[:]...)
return uint32(len(data)), 0
}
var _ fs.FileFlusher = (*bytesFileWriteHandle)(nil)
func (fh *bytesFileWriteHandle) Flush(ctx context.Context) (errno syscall.Errno) {
log.Debug("FUSE Attempting flush").Str("path", fh.path).Send()
fp, err := myfs.Create(fh.path, uint32(len(fh.content)))
if err != nil {
log.Error("FUSE Flush failed: create").Str("path", fh.path).Err(err).Send()
return syscallErr(err)
}
if len(fh.content) == 0 {
log.Debug("FUSE Flush no data to write").Str("path", fh.path).Send()
err = fp.Close()
if err != nil {
log.Error("FUSE Flush failed during close").Str("path", fh.path).Err(err).Send()
return syscallErr(err)
}
return 0
}
go func() {
// For every progress event
for sent := range fp.Progress() {
log.Debug("FUSE Flush progress").Int("bytes", int(sent)).Int("total", len(fh.content)).Send()
}
}()
r := bytes.NewReader(fh.content)
nread, err := io.Copy(fp, r)
if err != nil {
log.Error("FUSE Flush failed during write").Str("path", fh.path).Err(err).Send()
fp.Close()
return syscallErr(err)
}
if int(nread) != len(fh.content) {
log.Error("FUSE Flush failed during write").Str("path", fh.path).Int("expect", len(fh.content)).Int("got", int(nread)).Send()
fp.Close()
return syscall.EIO
}
err = fp.Close()
if err != nil {
log.Error("FUSE Flush failed during close").Str("path", fh.path).Err(err).Send()
return syscallErr(err)
}
log.Debug("FUSE Flush done").Str("path", fh.path).Int("size", len(fh.content)).Send()
return 0
}
var _ fs.FileFsyncer = (*bytesFileWriteHandle)(nil)
func (fh *bytesFileWriteHandle) Fsync(ctx context.Context, flags uint32) (errno syscall.Errno) {
return fh.Flush(ctx)
}
var _ fs.NodeGetattrer = (*ITNode)(nil)
func (bn *ITNode) Getattr(ctx context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno {
log.Debug("FUSE getattr").Str("path", bn.path).Send()
out.Ino = bn.Ino
out.Mtime = bn.self.modtime
out.Ctime = bn.self.modtime
out.Atime = bn.self.modtime
out.Size = uint64(bn.self.size)
return 0
}
var _ fs.NodeSetattrer = (*ITNode)(nil)
func (bn *ITNode) Setattr(ctx context.Context, fh fs.FileHandle, in *fuse.SetAttrIn, out *fuse.AttrOut) syscall.Errno {
log.Debug("FUSE setattr").Str("path", bn.path).Send()
out.Size = 0
out.Mtime = 0
return 0
}
var _ fs.NodeOpener = (*ITNode)(nil)
func (f *ITNode) Open(ctx context.Context, openFlags uint32) (fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) {
switch f.kind {
case 2:
// FS file
if openFlags&syscall.O_RDWR != 0 {
log.Error("FUSE Open failed: RDWR").Str("path", f.path).Send()
return nil, 0, syscall.EROFS
}
if openFlags&syscall.O_WRONLY != 0 {
log.Debug("FUSE Opening for write").Str("path", f.path).Send()
fh = &bytesFileWriteHandle{
path: f.path,
content: make([]byte, 0),
}
return fh, fuse.FOPEN_DIRECT_IO, 0
} else {
log.Debug("FUSE Opening for read").Str("path", f.path).Send()
fp, err := myfs.Open(f.path)
if err != nil {
log.Error("FUSE: Opening failed").Str("path", f.path).Err(err).Send()
return nil, 0, syscallErr(err)
}
defer fp.Close()
b := &bytes.Buffer{}
go func() {
// For every progress event
for sent := range fp.Progress() {
log.Debug("FUSE Read progress").Int("bytes", int(sent)).Int("total", int(f.self.size)).Send()
}
}()
_, err = io.Copy(b, fp)
if err != nil {
log.Error("FUSE Read failed").Str("path", f.path).Err(err).Send()
fp.Close()
return nil, 0, syscallErr(err)
}
fh = &bytesFileReadHandle{
content: b.Bytes(),
}
return fh, fuse.FOPEN_DIRECT_IO, 0
}
case 3:
// Device file
// disallow writes
if openFlags&(syscall.O_RDWR|syscall.O_WRONLY) != 0 {
return nil, 0, syscall.EROFS
}
for _, value := range properties {
if value.Ino == f.Ino {
ans, err := value.gen()
if err != nil {
return nil, 0, syscallErr(err)
}
fh = &sensorFileReadHandle{
content: ans,
}
return fh, fuse.FOPEN_DIRECT_IO, 0
}
}
}
return nil, 0, syscall.EINVAL
}
var _ fs.NodeCreater = (*ITNode)(nil)
func (f *ITNode) Create(ctx context.Context, name string, flags uint32, mode uint32, out *fuse.EntryOut) (node *fs.Inode, fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) {
if f.kind != 2 {
return nil, nil, 0, syscall.EROFS
}
path := f.path + "/" + name
ino := uint64(len(inodemap)) + 11
inodemap[path] = ino
stable := fs.StableAttr{
Mode: fuse.S_IFREG,
Ino: ino,
}
operations := &ITNode{
kind: nodeKindFS, Ino: ino,
path: path,
}
node = f.NewInode(ctx, operations, stable)
fh = &bytesFileWriteHandle{
path: path,
content: make([]byte, 0),
}
log.Debug("FUSE Creating file").Str("path", path).Send()
errno = 0
return node, fh, fuseFlags, 0
}
var _ fs.NodeMkdirer = (*ITNode)(nil)
func (f *ITNode) Mkdir(ctx context.Context, name string, mode uint32, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) {
if f.kind != 2 {
return nil, syscall.EROFS
}
path := f.path + "/" + name
err := myfs.Mkdir(path)
if err != nil {
log.Error("FUSE Mkdir failed").
Str("path", path).
Err(err).
Send()
return nil, syscallErr(err)
}
ino := uint64(len(inodemap)) + 11
inodemap[path] = ino
stable := fs.StableAttr{
Mode: fuse.S_IFDIR,
Ino: ino,
}
operations := &ITNode{
kind: nodeKindFS, Ino: ino,
path: path,
}
node := f.NewInode(ctx, operations, stable)
log.Debug("FUSE Mkdir success").
Str("path", path).
Int("ino", int(ino)).
Send()
return node, 0
}
var _ fs.NodeRenamer = (*ITNode)(nil)
func (f *ITNode) Rename(ctx context.Context, name string, newParent fs.InodeEmbedder, newName string, flags uint32) syscall.Errno {
if f.kind != 2 {
return syscall.EROFS
}
p1 := f.path + "/" + name
p2 := newParent.EmbeddedInode().Path(nil)[2:] + "/" + newName
err := myfs.Rename(p1, p2)
if err != nil {
log.Error("FUSE Rename failed").
Str("src", p1).
Str("dest", p2).
Err(err).
Send()
return syscallErr(err)
}
log.Debug("FUSE Rename sucess").
Str("src", p1).
Str("dest", p2).
Send()
ino := inodemap[p1]
delete(inodemap, p1)
inodemap[p2] = ino
return 0
}
var _ fs.NodeUnlinker = (*ITNode)(nil)
func (f *ITNode) Unlink(ctx context.Context, name string) syscall.Errno {
if f.kind != 2 {
return syscall.EROFS
}
delete(inodemap, f.path+"/"+name)
err := myfs.Remove(f.path + "/" + name)
if err != nil {
log.Error("FUSE Unlink failed").
Str("file", f.path+"/"+name).
Err(err).
Send()
return syscallErr(err)
}
log.Debug("FUSE Unlink success").
Str("file", f.path+"/"+name).
Send()
return 0
}
var _ fs.NodeRmdirer = (*ITNode)(nil)
func (f *ITNode) Rmdir(ctx context.Context, name string) syscall.Errno {
return f.Unlink(ctx, name)
}

View File

@ -0,0 +1,74 @@
package fusefs
import (
"syscall"
"go.arsenm.dev/infinitime/blefs"
)
func syscallErr(err error) syscall.Errno {
if err == nil {
return 0
}
switch err := err.(type) {
case blefs.FSError:
switch err.Code {
case 0x02: // filesystem error
return syscall.EIO // TODO
case 0x05: // read-only filesystem
return syscall.EROFS
case 0x03: // no such file
return syscall.ENOENT
case 0x04: // protocol error
return syscall.EPROTO
case -5: // input/output error
return syscall.EIO
case -84: // filesystem is corrupted
return syscall.ENOTRECOVERABLE // TODO
case -2: // no such directory entry
return syscall.ENOENT
case -17: // entry already exists
return syscall.EEXIST
case -20: // entry is not a directory
return syscall.ENOTDIR
case -39: // directory is not empty
return syscall.ENOTEMPTY
case -9: // bad file number
return syscall.EBADF
case -27: // file is too large
return syscall.EFBIG
case -22: // invalid parameter
return syscall.EINVAL
case -28: // no space left on device
return syscall.ENOSPC
case -12: // no more memory available
return syscall.ENOMEM
case -61: // no attr available
return syscall.ENODATA // TODO
case -36: // file name is too long
return syscall.ENAMETOOLONG
}
default:
switch err {
case blefs.ErrFileNotExists: // file does not exist
return syscall.ENOENT
case blefs.ErrFileReadOnly: // file is read only
return syscall.EACCES
case blefs.ErrFileWriteOnly: // file is write only
return syscall.EACCES
case blefs.ErrInvalidOffset: // invalid file offset
return syscall.EINVAL
case blefs.ErrOffsetChanged: // offset has already been changed
return syscall.ESPIPE
case blefs.ErrReadOpen: // only one file can be opened for reading at a time
return syscall.ENFILE
case blefs.ErrWriteOpen: // only one file can be opened for writing at a time
return syscall.ENFILE
case blefs.ErrNoRemoveRoot: // refusing to remove root directory
return syscall.EPERM
}
}
return syscall.EIO
}

View File

@ -0,0 +1,17 @@
package fusefs
import (
_ "unsafe"
"github.com/hanwen/go-fuse/v2/fuse"
)
func Unmount(mountPoint string) error {
return unmount(mountPoint, &fuse.MountOptions{DirectMount: false})
}
// Unfortunately, the FUSE library does not export its unmount function,
// so this is required until that changes
//
//go:linkname unmount github.com/hanwen/go-fuse/v2/fuse.unmount
func unmount(mountPoint string, opts *fuse.MountOptions) error

77
main.go
View File

@ -26,6 +26,8 @@ import (
"os"
"os/signal"
"strconv"
"sync"
"syscall"
"time"
@ -74,21 +76,7 @@ func main() {
LogLevel: level,
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
sigCh := make(chan os.Signal, 1)
go func() {
<-sigCh
cancel()
time.Sleep(200 * time.Millisecond)
os.Exit(0)
}()
signal.Notify(
sigCh,
syscall.SIGINT,
syscall.SIGTERM,
)
ctx := context.Background()
// Connect to InfiniTime with default options
dev, err := infinitime.Connect(ctx, opts)
@ -145,49 +133,88 @@ func main() {
log.Error("Error setting current time on connected InfiniTime").Err(err).Send()
}
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
ctx, cancel := context.WithCancel(ctx)
defer cancel()
go func() {
sig := <-sigCh
log.Warn("Signal received, shutting down").Stringer("signal", sig).Send()
cancel()
}()
wg := WaitGroup{&sync.WaitGroup{}}
// Initialize music controls
err = initMusicCtrl(ctx, dev)
err = initMusicCtrl(ctx, wg, dev)
if err != nil {
log.Error("Error initializing music control").Err(err).Send()
}
// Start control socket
err = initCallNotifs(ctx, dev)
err = initCallNotifs(ctx, wg, dev)
if err != nil {
log.Error("Error initializing call notifications").Err(err).Send()
}
// Initialize notification relay
err = initNotifRelay(ctx, dev)
err = initNotifRelay(ctx, wg, dev)
if err != nil {
log.Error("Error initializing notification relay").Err(err).Send()
}
// Initializa weather
err = initWeather(ctx, dev)
err = initWeather(ctx, wg, dev)
if err != nil {
log.Error("Error initializing weather").Err(err).Send()
}
// Initialize metrics collection
err = initMetrics(ctx, dev)
err = initMetrics(ctx, wg, dev)
if err != nil {
log.Error("Error intializing metrics collection").Err(err).Send()
}
// Initialize metrics collection
err = initPureMaps(ctx, dev)
// Initialize puremaps integration
err = initPureMaps(ctx, wg, dev)
if err != nil {
log.Error("Error intializing puremaps integration").Err(err).Send()
}
// Start fuse socket
if k.Bool("fuse.enabled") {
err = startFUSE(ctx, wg, dev)
if err != nil {
log.Error("Error starting fuse socket").Err(err).Send()
}
}
// Start control socket
err = startSocket(ctx, dev)
err = startSocket(ctx, wg, dev)
if err != nil {
log.Error("Error starting socket").Err(err).Send()
}
// Block forever
select {}
wg.Wait()
}
type x struct {
n int
*sync.WaitGroup
}
func (xy *x) Add(i int) {
xy.n += i
xy.WaitGroup.Add(i)
fmt.Println("add: counter:", xy.n)
}
func (xy *x) Done() {
xy.n -= 1
xy.WaitGroup.Done()
fmt.Println("done: counter:", xy.n)
}
func onReqPasskey() (uint32, error) {

View File

@ -18,7 +18,7 @@ const (
progressProperty = interfaceName + ".progress"
)
func initPureMaps(ctx context.Context, dev *infinitime.Device) error {
func initPureMaps(ctx context.Context, wg WaitGroup, dev *infinitime.Device) error {
// Connect to session bus. This connection is for method calls.
conn, err := utils.NewSessionBusConn(ctx)
if err != nil {
@ -59,7 +59,10 @@ func initPureMaps(ctx context.Context, dev *infinitime.Device) error {
}
}
wg.Add(1)
go func() {
defer wg.Done("pureMaps")
signalCh := make(chan *dbus.Message, 10)
monitorConn.Eavesdrop(signalCh)

View File

@ -4,6 +4,7 @@ import (
"context"
"database/sql"
"path/filepath"
"time"
"go.arsenm.dev/infinitime"
@ -11,7 +12,7 @@ import (
_ "modernc.org/sqlite"
)
func initMetrics(ctx context.Context, dev *infinitime.Device) error {
func initMetrics(ctx context.Context, wg WaitGroup, dev *infinitime.Device) error {
// If metrics disabled, return nil
if !k.Bool("metrics.enabled") {
return nil
@ -125,6 +126,13 @@ func initMetrics(ctx context.Context, dev *infinitime.Device) error {
}()
}
wg.Add(1)
go func() {
defer wg.Done("metrics")
<-ctx.Done()
db.Close()
}()
log.Info("Initialized metrics collection").Send()
return nil

View File

@ -20,6 +20,7 @@ package main
import (
"context"
"go.arsenm.dev/infinitime"
"go.arsenm.dev/itd/mpris"
@ -27,7 +28,7 @@ import (
"go.arsenm.dev/logger/log"
)
func initMusicCtrl(ctx context.Context, dev *infinitime.Device) error {
func initMusicCtrl(ctx context.Context, wg WaitGroup, dev *infinitime.Device) error {
mpris.Init(ctx)
maps := k.Strings("notifs.translit.use")
@ -54,23 +55,31 @@ func initMusicCtrl(ctx context.Context, dev *infinitime.Device) error {
if err != nil {
return err
}
wg.Add(1)
go func() {
defer wg.Done("musicCtrl")
// For every music event received
for musicEvt := range musicEvtCh {
// Perform appropriate action based on event
switch musicEvt {
case infinitime.MusicEventPlay:
mpris.Play()
case infinitime.MusicEventPause:
mpris.Pause()
case infinitime.MusicEventNext:
mpris.Next()
case infinitime.MusicEventPrev:
mpris.Prev()
case infinitime.MusicEventVolUp:
mpris.VolUp(uint(k.Int("music.vol.interval")))
case infinitime.MusicEventVolDown:
mpris.VolDown(uint(k.Int("music.vol.interval")))
for {
select {
case musicEvt := <-musicEvtCh:
// Perform appropriate action based on event
switch musicEvt {
case infinitime.MusicEventPlay:
mpris.Play()
case infinitime.MusicEventPause:
mpris.Pause()
case infinitime.MusicEventNext:
mpris.Next()
case infinitime.MusicEventPrev:
mpris.Prev()
case infinitime.MusicEventVolUp:
mpris.VolUp(uint(k.Int("music.vol.interval")))
case infinitime.MusicEventVolDown:
mpris.VolDown(uint(k.Int("music.vol.interval")))
}
case <-ctx.Done():
return
}
}
}()

View File

@ -29,7 +29,7 @@ import (
"go.arsenm.dev/logger/log"
)
func initNotifRelay(ctx context.Context, dev *infinitime.Device) error {
func initNotifRelay(ctx context.Context, wg WaitGroup, dev *infinitime.Device) error {
// Connect to dbus session bus
bus, err := utils.NewSessionBusConn(ctx)
if err != nil {
@ -54,43 +54,51 @@ func initNotifRelay(ctx context.Context, dev *infinitime.Device) error {
// Send events to channel
bus.Eavesdrop(notifCh)
wg.Add(1)
go func() {
defer wg.Done("notifRelay")
// For every event sent to channel
for v := range notifCh {
// If firmware is updating, skip
if firmwareUpdating {
continue
for {
select {
case v := <-notifCh:
// If firmware is updating, skip
if firmwareUpdating {
continue
}
// If body does not contain 5 elements, skip
if len(v.Body) < 5 {
continue
}
// Get requred fields
sender, summary, body := v.Body[0].(string), v.Body[3].(string), v.Body[4].(string)
// If fields are ignored in config, skip
if ignored(sender, summary, body) {
continue
}
maps := k.Strings("notifs.translit.use")
translit.Transliterators["custom"] = translit.Map(k.Strings("notifs.translit.custom"))
sender = translit.Transliterate(sender, maps...)
summary = translit.Transliterate(summary, maps...)
body = translit.Transliterate(body, maps...)
var msg string
// If summary does not exist, set message to body.
// If it does, set message to summary, two newlines, and then body
if summary == "" {
msg = body
} else {
msg = fmt.Sprintf("%s\n\n%s", summary, body)
}
dev.Notify(sender, msg)
case <-ctx.Done():
bus.Close()
return
}
// If body does not contain 5 elements, skip
if len(v.Body) < 5 {
continue
}
// Get requred fields
sender, summary, body := v.Body[0].(string), v.Body[3].(string), v.Body[4].(string)
// If fields are ignored in config, skip
if ignored(sender, summary, body) {
continue
}
maps := k.Strings("notifs.translit.use")
translit.Transliterators["custom"] = translit.Map(k.Strings("notifs.translit.custom"))
sender = translit.Transliterate(sender, maps...)
summary = translit.Transliterate(summary, maps...)
body = translit.Transliterate(body, maps...)
var msg string
// If summary does not exist, set message to body.
// If it does, set message to summary, two newlines, and then body
if summary == "" {
msg = body
} else {
msg = fmt.Sprintf("%s\n\n%s", summary, body)
}
dev.Notify(sender, msg)
}
}()

View File

@ -25,6 +25,7 @@ import (
"net"
"os"
"path/filepath"
"time"
"go.arsenm.dev/drpc/muxserver"
@ -41,7 +42,7 @@ var (
ErrDFUInvalidUpgType = errors.New("invalid upgrade type")
)
func startSocket(ctx context.Context, dev *infinitime.Device) error {
func startSocket(ctx context.Context, wg WaitGroup, dev *infinitime.Device) error {
// Make socket directory if non-existant
err := os.MkdirAll(filepath.Dir(k.String("socket.path")), 0o755)
if err != nil {
@ -77,10 +78,13 @@ func startSocket(ctx context.Context, dev *infinitime.Device) error {
return err
}
go muxserver.New(mux).Serve(ctx, ln)
log.Info("Starting control socket").Str("path", k.String("socket.path")).Send()
// Log socket start
log.Info("Started control socket").Str("path", k.String("socket.path")).Send()
wg.Add(1)
go func() {
defer wg.Done("socket")
muxserver.New(mux).Serve(ctx, ln)
}()
return nil
}

16
waitgroup.go Normal file
View File

@ -0,0 +1,16 @@
package main
import (
"sync"
"go.arsenm.dev/logger/log"
)
type WaitGroup struct {
*sync.WaitGroup
}
func (wg WaitGroup) Done(c string) {
log.Info("Component stopped").Str("name", c).Send()
wg.WaitGroup.Done()
}

View File

@ -9,6 +9,7 @@ import (
"net/url"
"strconv"
"strings"
"time"
"go.arsenm.dev/infinitime"
@ -61,7 +62,14 @@ type OSMData []struct {
var sendWeatherCh = make(chan struct{}, 1)
func initWeather(ctx context.Context, dev *infinitime.Device) error {
func sleepCtx(ctx context.Context, d time.Duration) {
select {
case <-time.After(d):
case <-ctx.Done():
}
}
func initWeather(ctx context.Context, wg WaitGroup, dev *infinitime.Device) error {
if !k.Bool("weather.enabled") {
return nil
}
@ -74,14 +82,21 @@ func initWeather(ctx context.Context, dev *infinitime.Device) error {
timer := time.NewTimer(time.Hour)
wg.Add(1)
go func() {
defer wg.Done("weather")
for {
_, ok := <-ctx.Done()
if !ok {
return
}
// Attempt to get weather
data, err := getWeather(ctx, lat, lon)
if err != nil {
log.Warn("Error getting weather data").Err(err).Send()
// Wait 15 minutes before retrying
time.Sleep(15 * time.Minute)
sleepCtx(ctx, 15*time.Minute)
continue
}
@ -174,6 +189,8 @@ func initWeather(ctx context.Context, dev *infinitime.Device) error {
select {
case <-timer.C:
case <-sendWeatherCh:
case <-ctx.Done():
return
}
}
}()