diff --git a/.gitignore b/.gitignore index 47d3278..4395f33 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ config.yaml -midi-request-trigger \ No newline at end of file +midi-request-trigger +midi-request-trigger.log \ No newline at end of file diff --git a/config.go b/config.go index 7c9f824..5416d73 100644 --- a/config.go +++ b/config.go @@ -1,13 +1,17 @@ package main import ( - "log" + "fmt" + "io" "os" "os/user" "path" "path/filepath" + "runtime" "github.com/kkyr/fig" + log "github.com/sirupsen/logrus" + "gopkg.in/natefinch/lumberjack.v2" ) // Configurations relating to HTTP server. @@ -19,9 +23,127 @@ type HTTPConfig struct { Enabled bool `fig:"enabled"` } +// Configuration for logging. +type LogConfig struct { + // Limit the log output by the log level. + Level string `fig:"level" yaml:"level" enum:"debug,info,warn,error" default:"info"` + // How should the log output be formatted. + Type string `fig:"type" yaml:"type" enum:"json,console" default:"console"` + // The outputs that the log should go to. Output of `console` will + // go to the stderr. An file path, will log to the file. Using `default-file` + // it'll either save to `/var/log/name.log`, or to the same directory as the + // executable if the path is not writable, or on Windows. + Outputs []string `fig:"outputs" yaml:"outputs" default:"[console,default-file]"` + // Maximum size of the log file in megabytes before it gets rotated. + MaxSize int `fig:"max_size" yaml:"max_size" default:"1"` + // Maximum number of backups to save. + MaxBackups int `fig:"max_backups" yaml:"max_backups" default:"3"` + // Maximum number of days to retain old log files. + MaxAge int `fig:"max_age" yaml:"max_age" default:"0"` + // Use the logal system time instead of UTC for file names of rotated backups. + LocalTime *bool `fig:"local_time" yaml:"local_time" default:"true"` + // Should the rotated logs be compressed. + Compress *bool `fig:"compress" yaml:"compress" default:"true"` +} + +// Apply log config. +func (l *LogConfig) Apply() { + // Apply level. + switch l.Level { + case "debug": + log.SetLevel(log.DebugLevel) + case "info": + log.SetLevel(log.InfoLevel) + case "warn": + log.SetLevel(log.WarnLevel) + default: + log.SetLevel(log.ErrorLevel) + } + + // Apply type. + switch l.Type { + case "json": + log.SetFormatter(&log.JSONFormatter{}) + default: + log.SetFormatter(&log.TextFormatter{}) + } + + // Change the outputs. + var outputs []io.Writer + for _, output := range l.Outputs { + // If output is console, add stderr and continue. + if output == "console" { + outputs = append(outputs, os.Stderr) + continue + } + + // If default-file defined, find the default file. + if output == "default-file" { + var f *os.File + var err error + var logDir, logPath string + logName := fmt.Sprintf("%s.log", serviceName) + + // On *nix, `/var/log/` should be default if writable. + if runtime.GOOS != "windows" { + logDir = "/var/log" + logPath = filepath.Join(logDir, logName) + + // Verify we can write to log file. + f, err = os.OpenFile(logPath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644) + } + + // If we could not open the file, then we should try the executable path. + if err != nil || f == nil { + exe, err := os.Executable() + if err != nil { + log.Println("Unable to find an writable log path to save log to.") + continue + } else { + logDir = filepath.Dir(exe) + logPath = filepath.Join(logDir, logName) + + // Verify we can write to log file. + f, err = os.OpenFile(logPath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644) + if err != nil { + log.Println("Unable to find an writable log path to save log to.") + continue + } else { + f.Close() + } + } + } else { + // Close file. + f.Close() + } + + // Update the config log path. + output = logPath + } + + // Setup lumberjack log rotate for the output, and add to the list. + logFile := &lumberjack.Logger{ + Filename: output, + MaxSize: l.MaxSize, + MaxBackups: l.MaxBackups, + MaxAge: l.MaxAge, + LocalTime: *l.LocalTime, + Compress: *l.Compress, + } + outputs = append(outputs, logFile) + } + + // If there are outputs, set the outputs. + if len(outputs) != 0 { + mw := io.MultiWriter(outputs...) + log.SetOutput(mw) + } +} + // Configuration Structure. type Config struct { HTTP HTTPConfig `fig:"http"` + Log *LogConfig `fig:"log" yaml:"log"` MidiRouters []*MidiRouter `fig:"midi_routers"` } @@ -59,6 +181,7 @@ func (a *App) ReadConfig() { Debug: true, Enabled: false, }, + Log: &LogConfig{}, } // Load configuration. @@ -81,6 +204,9 @@ func (a *App) ReadConfig() { config.HTTP.Port = app.flags.HTTPPort } + // Apply log configs. + config.Log.Apply() + // Set global config structure. app.config = config } diff --git a/go.mod b/go.mod index 4aaf014..9bbde12 100644 --- a/go.mod +++ b/go.mod @@ -1,21 +1,27 @@ module github.com/GRMrGecko/midi-request-trigger -go 1.20 +go 1.24.2 + +toolchain go1.24.4 require ( github.com/eclipse/paho.mqtt.golang v1.5.0 - github.com/gorilla/handlers v1.5.1 - github.com/gorilla/mux v1.8.0 - github.com/kkyr/fig v0.3.2 - gitlab.com/gomidi/midi/v2 v2.0.30 + github.com/gorilla/handlers v1.5.2 + github.com/gorilla/mux v1.8.1 + github.com/kkyr/fig v0.5.0 + github.com/sirupsen/logrus v1.9.3 + gitlab.com/gomidi/midi/v2 v2.3.14 + gopkg.in/natefinch/lumberjack.v2 v2.2.1 ) require ( - github.com/felixge/httpsnoop v1.0.1 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect github.com/gorilla/websocket v1.5.3 // indirect - github.com/mitchellh/mapstructure v1.4.1 // indirect - github.com/pelletier/go-toml v1.9.3 // indirect - golang.org/x/net v0.27.0 // indirect - golang.org/x/sync v0.7.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/pelletier/go-toml v1.9.5 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + golang.org/x/net v0.41.0 // indirect + golang.org/x/sync v0.15.0 // indirect + golang.org/x/sys v0.33.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 1ca0566..fe0da64 100644 --- a/go.sum +++ b/go.sum @@ -1,26 +1,64 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/eclipse/paho.mqtt.golang v1.5.0 h1:EH+bUVJNgttidWFkLLVKaQPGmkTUfQQqjOsyvMGvD6o= github.com/eclipse/paho.mqtt.golang v1.5.0/go.mod h1:du/2qNQVqJf/Sqs4MEL77kR8QTqANF7XU7Fk0aOTAgk= github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ= github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= +github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= +github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/kkyr/fig v0.3.2 h1:+vMj52FL6RJUxeKOBB6JXIMyyi1/2j1ERDrZXjoBjzM= github.com/kkyr/fig v0.3.2/go.mod h1:ItUILF8IIzgZOMhx5xpJ1W/bviQsWRKOwKXfE/tqUoA= +github.com/kkyr/fig v0.5.0 h1:D4ym5MYYScOSgqyx1HYQaqFn9dXKzIuSz8N6SZ4rzqM= +github.com/kkyr/fig v0.5.0/go.mod h1:U4Rq/5eUNJ8o5UvOEc9DiXtNf41srOLn2r/BfCyuc58= github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ= github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +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/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= gitlab.com/gomidi/midi/v2 v2.0.30 h1:RgRYbQeQSab5ZaP1lqRcCTnTSBQroE3CE6V9HgMmOAc= gitlab.com/gomidi/midi/v2 v2.0.30/go.mod h1:Y6IFFyABN415AYsFMPJb0/43TRIuVYDpGKp2gDYLTLI= +gitlab.com/gomidi/midi/v2 v2.3.14 h1:BbTDExFlg0zm90AtyGDdO87jdKjn+eYqeSlSGGpFPzQ= +gitlab.com/gomidi/midi/v2 v2.3.14/go.mod h1:jDpP4O4skYi+7iVwt6Zyp18bd2M4hkjtMuw2cmgKgfw= golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/http.go b/http.go index 8fc1f5b..b3a4d9d 100644 --- a/http.go +++ b/http.go @@ -4,13 +4,13 @@ import ( "context" "fmt" "io" - "log" "net" "net/http" "os" "github.com/gorilla/handlers" "github.com/gorilla/mux" + log "github.com/sirupsen/logrus" ) // Basic HTTP server structure. diff --git a/main.go b/main.go index 9c4dfe5..dca5377 100644 --- a/main.go +++ b/main.go @@ -3,11 +3,11 @@ package main import ( "context" "fmt" - "log" "os" "os/signal" "syscall" + log "github.com/sirupsen/logrus" "gitlab.com/gomidi/midi/v2" _ "gitlab.com/gomidi/midi/v2/drivers/rtmididrv" ) @@ -15,7 +15,7 @@ import ( const ( serviceName = "midi-request-trigger" serviceDescription = "Takes trigger MIDI messages by HTTP or MQTT requests and trigger HTTP or MQTT requests by MIDI messages" - serviceVersion = "0.3" + serviceVersion = "0.4.1" ) // App is the global application structure for communicating between servers and storing information. diff --git a/midiRouter.go b/midiRouter.go index 4b10704..58d41e3 100644 --- a/midiRouter.go +++ b/midiRouter.go @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" "io" - "log" "net/http" "net/url" "regexp" @@ -14,6 +13,7 @@ import ( "time" mqtt "github.com/eclipse/paho.mqtt.golang" + log "github.com/sirupsen/logrus" "gitlab.com/gomidi/midi/v2" "gitlab.com/gomidi/midi/v2/drivers" ) @@ -128,7 +128,7 @@ type RequestTrigger struct { type MidiRouter struct { // Used for human readable config. Name string `fig:"name"` - // Midi device to connect. + // Midi device to connect, accepts regular expression. Device string `fig:"device"` // MQTT Connection if you are to integrate with MQTT. MQTT MQTTConfig `fig:"mqtt"` @@ -480,10 +480,23 @@ func (r *MidiRouter) Connect() { // If request triggers defined, find the out port. if len(r.RequestTriggers) != 0 { go func() { + deviceRx, err := regexp.Compile(r.Device) + if err != nil { + log.Printf("Failed to compile regexp of '%s': %v", r.Device, err) + } for { - out, err := midi.FindOutPort(r.Device) + var out drivers.Out + for _, device := range midi.GetOutPorts() { + if deviceRx.MatchString(device.String()) { + err = device.Open() + out = device + } + } + if out == nil { + err = fmt.Errorf("unable to find matching device") + } if err != nil { - r.Log(ErrorLog, "Can't find output device: %s", r.Device) + r.Log(ErrorLog, "Failed to find output device '%s': %v", r.Device, err) } else { r.MidiOut = out break @@ -498,12 +511,25 @@ func (r *MidiRouter) Connect() { // If listener is disabled, stop here. if !r.DisableListener { go func() { + deviceRx, err := regexp.Compile(r.Device) + if err != nil { + log.Printf("Failed to compile regexp of '%s': %v", r.Device, err) + } for { // Try finding input port. r.Log(InfoLog, "Connecting to input device: %s", r.Device) - in, err := midi.FindInPort(r.Device) + var in drivers.In + for _, device := range midi.GetInPorts() { + if deviceRx.MatchString(device.String()) { + err = device.Open() + in = device + } + } + if in == nil { + err = fmt.Errorf("unable to find matching device") + } if err != nil { - r.Log(ErrorLog, "Can't find input device: %s", r.Device) + r.Log(ErrorLog, "Can't find input device '%s': %v", r.Device, err) r.Log(ErrorLog, "Retrying in 1 minute.") time.Sleep(time.Minute) continue