diff --git a/node/icecast.go b/node/icecast.go index 8fe19d8a929c0dd3ffaa26f2cf515e058f9e7d61..af875705a4b2f450a3f758a19962843dac663a2e 100644 --- a/node/icecast.go +++ b/node/icecast.go @@ -3,6 +3,7 @@ package node import ( "encoding/xml" "errors" + "flag" "fmt" "io" "log" @@ -18,21 +19,63 @@ import ( "git.autistici.org/ale/autoradio/util" ) +// Managing the local Icecast instance is a less simple task than it +// might look, as the details tend to be distribution-specific. We'd +// like to avoid implementing yet another service manager within +// radiod, so we are just going to assume that the Icecast daemon is +// managed using the local distribution-specific tools, and is running +// independently of radiod. Furthermore, we're going to make the +// following assumptions: +// +// - We have permissions to modify the Icecast config file; +// +// - We have installed our status XSLT page; +// +// - We can reload the Icecast service (send the icecast2 process a +// SIGHUP). Ideally we'd like to use the distribution-specific method, +// but with systemd we need to use "sudo". The code tries to +// autodetect a supported mechanism. +// +// The above is usually accomplished by running radiod as the same +// user that is running the Icecast daemon. +// var ( - statusPage = "/status-autoradio.xsl" - icecastConfigFile = "/etc/icecast2/icecast.xml" - icecastReloadCmd = "/usr/sbin/service icecast2 reload || /usr/sbin/service icecast2 start" + icecastConfigFile = flag.String("icecast-config", "/etc/icecast2/icecast.xml", "Icecast configuration file") + icecastCustomReloadCmd = flag.String("icecast-reload-command", "", "Command to reload / restart the icecast2 daemon") - icecastOk = instrumentation.NewGauge("icecast.ok") + icecastStatusPage = "/status-autoradio.xsl" + icecastOk = instrumentation.NewGauge("icecast.ok") ) +const ( + systemCtlReloadCmd = "sudo systemctl reload icecast2.service || sudo systemctl restart icecast2.service" + debianReloadCmd = "/usr/sbin/service icecast2 reload || /usr/sbin/service icecast2 restart" + genericReloadCmd = "pkill -HUP icecast2" +) + +func getIcecastReloadCmd() string { + if *icecastCustomReloadCmd != "" { + return *icecastCustomReloadCmd + } + if _, err := os.Stat("/bin/systemctl"); err == nil { + log.Printf("using /bin/systemctl to reload icecast2") + return systemCtlReloadCmd + } + if _, err := os.Stat("/usr/sbin/service"); err == nil { + log.Printf("using /usr/sbin/service to reload icecast2") + return debianReloadCmd + } + log.Printf("using pkill to reload icecast2") + return genericReloadCmd +} + // Icecast returns empty fields in our status handler, which we'll // need to turn into integers (the xml unmarshaler will return an // error in this specific case), so we use a separate type for // decoding the status page output. This would be much simpler if I // knew how to get the XSLT to put a default value in the output // instead of an empty field... -type icecastMountStatusUnparsed struct { +type icecastMountStatusRaw struct { Name string `xml:"name,attr"` Listeners string `xml:"listeners"` BitRate string `xml:"bitrate"` @@ -42,9 +85,9 @@ type icecastMountStatusUnparsed struct { FrameRate string `xml:"frame-rate"` } -type icecastStatusUnparsed struct { - XMLName xml.Name `xml:"status"` - Mounts []icecastMountStatusUnparsed `xml:"mount"` +type icecastStatusRaw struct { + XMLName xml.Name `xml:"status"` + Mounts []icecastMountStatusRaw `xml:"mount"` } type icecastStatus struct { @@ -53,15 +96,17 @@ type icecastStatus struct { } type icecastController struct { - config *icecastConfig - status *icecastStatus - stop chan bool + config *icecastConfig + status *icecastStatus + reloadCmd string + stop chan bool } func newIcecastController(publicIP string, maxClients int) *icecastController { return &icecastController{ - config: newIcecastConfig(publicIP, maxClients), - status: &icecastStatus{}, + config: newIcecastConfig(publicIP, maxClients), + status: &icecastStatus{}, + reloadCmd: getIcecastReloadCmd(), } } @@ -69,7 +114,8 @@ func newIcecastController(publicIP string, maxClients int) *icecastController { // for debugging purposes. func (ic *icecastController) reload() error { log.Printf("reloading icecast") - cmd := exec.Command("/bin/sh", "-c", icecastReloadCmd) + cmd := exec.Command("/bin/sh", "-c", ic.reloadCmd) + cmd.Dir = "/" cmd.Stdout = os.Stderr cmd.Stderr = os.Stderr return cmd.Run() @@ -113,7 +159,7 @@ func (ic *icecastController) Update(conf *clusterConfig, isMaster bool, masterAd return err } - changed, err := util.WriteFileIfChanged(icecastConfigFile, data) + changed, err := util.WriteFileIfChanged(*icecastConfigFile, data) if err != nil { return err } @@ -163,7 +209,7 @@ func (ic *icecastController) Run(stop chan bool) { } func (ic *icecastController) fetchStatus() (*icecastStatus, error) { - resp, err := http.Get(fmt.Sprintf("http://localhost:%d%s", autoradio.IcecastPort, statusPage)) + resp, err := http.Get(fmt.Sprintf("http://localhost:%d%s", autoradio.IcecastPort, icecastStatusPage)) if err != nil { return nil, err } @@ -172,7 +218,7 @@ func (ic *icecastController) fetchStatus() (*icecastStatus, error) { } func (ic *icecastController) parseStatusPage(input io.Reader) (*icecastStatus, error) { - var ustatus icecastStatusUnparsed + var ustatus icecastStatusRaw if err := xml.NewDecoder(input).Decode(&ustatus); err != nil { return nil, err }