Skip to content
Snippets Groups Projects
Commit 2f3a1b45 authored by ale's avatar ale
Browse files

only reload icecast if the configuration file changed

This will limit useless reloads (and related killSources), for instance
when transcoding parameters change. Also, improve the icecast
config generation test.
parent 1748f0f0
No related branches found
No related tags found
No related merge requests found
......@@ -15,6 +15,7 @@ import (
"git.autistici.org/ale/autoradio"
"git.autistici.org/ale/autoradio/instrumentation"
"git.autistici.org/ale/autoradio/util"
)
var (
......@@ -67,6 +68,7 @@ func NewIcecastController(publicIp string, maxClients int) *icecastController {
// Reload the icecast daemon. Redirects output to our standard error
// for debugging purposes.
func (ic *icecastController) reload() error {
log.Printf("reloading icecast")
cmd := exec.Command("/bin/sh", "-c", icecastReloadCmd)
cmd.Stdout = os.Stderr
cmd.Stderr = os.Stderr
......@@ -74,27 +76,28 @@ func (ic *icecastController) reload() error {
}
// Kill sources connected to local streams.
func (ic *icecastController) killSources(conf *clusterConfig) error {
var anyErr error
func (ic *icecastController) killSources(conf *clusterConfig) {
client := &http.Client{}
for _, m := range conf.ListMounts() {
req, err := http.NewRequest("GET", fmt.Sprintf("http://localhost:%d/admin/killsource?mount=%s", autoradio.IcecastPort, autoradio.MountNameToIcecastPath(m.Name)), nil)
if err != nil {
anyErr = err
log.Printf("killSources: %v", err)
continue
}
req.SetBasicAuth("admin", getIcecastAdminPassword())
resp, err := client.Do(req)
if err != nil {
anyErr = err
log.Printf("killSources: %v", err)
continue
}
resp.Body.Close()
if resp.StatusCode != 200 {
anyErr = fmt.Errorf("HTTP status %s", resp.Status)
// Do not report errors when killing sources on relay
// mounts - we only do it in the off chance that the
// mount was *not* a relay before.
if !m.IsRelay() && resp.StatusCode != 200 {
log.Printf("killSources: HTTP status %s", resp.Status)
}
}
return anyErr
}
// Update reloads the Icecast daemon with a new configuration.
......@@ -103,28 +106,36 @@ func (ic *icecastController) Update(conf *clusterConfig, isMaster bool, masterAd
return errors.New("unknown system state")
}
// Try to kill sources connected to the local icecast daemon
// before reloading, otherwise we'll have problems on master
// -> slave transitions (for example, on a restart of radiod)
// as sources will be "stuck" preventing the new configuration
// from taking effect.
if err := ic.killSources(conf); err != nil {
log.Printf("error killing sources: %v", err)
}
// Write a new configuration (atomically).
ic.config.Update(conf, isMaster, masterAddr.String())
tmpf := icecastConfigFile + ".tmp"
defer os.Remove(tmpf)
if err := ic.config.EncodeToFile(tmpf); err != nil {
data, err := ic.config.Encode()
if err != nil {
return err
}
if err := os.Rename(tmpf, icecastConfigFile); err != nil {
changed, err := util.WriteFileIfChanged(icecastConfigFile, data)
if err != nil {
return err
}
if changed {
// Try to kill sources connected to the local icecast daemon
// before reloading, otherwise we'll have problems on master
// -> slave transitions (for example, on a restart of radiod)
// as sources will be "stuck" preventing the new configuration
// from taking effect.
//
// There is unfortunately a small race condition here, as it
// is possible for sources to reconnect before we reload the
// icecast daemon.
//
// TODO: only kill sources whose configuration has changed.
ic.killSources(conf)
// Reload the icecast daemon.
return ic.reload()
}
// Tell the Icecast daemon to reload its configuration.
return ic.reload()
return nil
}
func (ic *icecastController) GetStatus() *IcecastStatus {
......
......@@ -9,7 +9,7 @@ import (
"log"
"net"
"net/url"
"os"
"sort"
"strconv"
"git.autistici.org/ale/autoradio"
......@@ -195,22 +195,6 @@ func (c *icecastConfig) Encode() ([]byte, error) {
return buf.Bytes(), nil
}
// EncodeToFile writes the configuration to a file.
func (c *icecastConfig) EncodeToFile(path string) error {
file, err := os.Create(path)
if err != nil {
return err
}
defer file.Close()
data, err := c.Encode()
if err != nil {
return err
}
_, err = file.Write(data)
return err
}
func masterMountToIcecastConfig(m *autoradio.Mount) iceMountConfig {
mconfig := iceMountConfig{
Name: autoradio.MountNameToIcecastPath(m.Name),
......@@ -274,14 +258,26 @@ func slaveMountToIcecastConfig(masterAddr string, m *autoradio.Mount) iceRelayCo
}
}
type mountList []*autoradio.Mount
func (l mountList) Len() int { return len(l) }
func (l mountList) Swap(i, j int) { l[i], l[j] = l[j], l[i] }
func (l mountList) Less(i, j int) bool {
return l[i].Name < l[j].Name
}
// Update the configuration with the current list of mounts and
// masterelection state. This will clear the Mounts and Relays fields
// and set them to new values.
// and set them to new values. The mounts are sorted by name so that
// the XML representation generated by Encode() is consistent.
func (ic *icecastConfig) Update(config *clusterConfig, isMaster bool, masterAddr string) {
var mounts []iceMountConfig
var relays []iceRelayConfig
for _, m := range config.ListMounts() {
allMounts := config.ListMounts()
sort.Sort(mountList(allMounts))
for _, m := range allMounts {
switch {
case m.IsRelay():
if rc, ok := relayToIcecastConfig(m); ok {
......
package node
import (
"bytes"
"fmt"
"strings"
"testing"
"git.autistici.org/ale/autoradio"
"github.com/aryann/difflib"
)
const (
expectedSlaveConfig = `<!-- Automatically generated, do not edit -->
<icecast>
<limits>
<clients>1000</clients>
<sources>500</sources>
<queue-size>1048576</queue-size>
<client-timeout>30</client-timeout>
<header-timeout>15</header-timeout>
<source-timeout>60</source-timeout>
<burst-size>131072</burst-size>
</limits>
<authentication>
<source-password>sourcepass</source-password>
<admin-user>admin</admin-user>
<admin-password>adminpass</admin-password>
</authentication>
<hostname>1.2.3.4</hostname>
<fileserve>1</fileserve>
<paths>
<basedir>/usr/share/icecast2</basedir>
<logdir>/var/log/icecast2</logdir>
<webroot>/usr/share/icecast2/web</webroot>
<adminroot>/usr/share/icecast2/admin</adminroot>
</paths>
<logging>
<accesslog>access.log</accesslog>
<errorlog>error.log</errorlog>
<loglevel>3</loglevel>
<logsize>10000</logsize>
</logging>
<security>
<chroot>0</chroot>
</security>
<listen-socket>
<bind-address>0.0.0.0</bind-address>
<port>8000</port>
<shoutcast-compat>0</shoutcast-compat>
</listen-socket>
<relay>
<server>2.3.4.5</server>
<port>8000</port>
<mount>/_stream/stream1.ogg</mount>
<local-mount>/_stream/stream1.ogg</local-mount>
<username>user</username>
<password>pass</password>
<on-demand>1</on-demand>
<relay-shoutcast-metadata>1</relay-shoutcast-metadata>
</relay>
<relay>
<server>example.com</server>
<port>80</port>
<mount>/stream2.ogg</mount>
<local-mount>/_stream/stream2.ogg</local-mount>
<username></username>
<password></password>
<on-demand>1</on-demand>
<relay-shoutcast-metadata>1</relay-shoutcast-metadata>
</relay>
<relay>
<server>2.3.4.5</server>
<port>8000</port>
<mount>/_stream/stream3.mp3</mount>
<local-mount>/_stream/stream3.mp3</local-mount>
<username>user</username>
<password>pass</password>
<on-demand>1</on-demand>
<relay-shoutcast-metadata>1</relay-shoutcast-metadata>
</relay>
</icecast>`
expectedMasterConfig = `<!-- Automatically generated, do not edit -->
<icecast>
<limits>
<clients>1000</clients>
<sources>500</sources>
<queue-size>1048576</queue-size>
<client-timeout>30</client-timeout>
<header-timeout>15</header-timeout>
<source-timeout>60</source-timeout>
<burst-size>131072</burst-size>
</limits>
<authentication>
<source-password>sourcepass</source-password>
<admin-user>admin</admin-user>
<admin-password>adminpass</admin-password>
</authentication>
<hostname>1.2.3.4</hostname>
<fileserve>1</fileserve>
<paths>
<basedir>/usr/share/icecast2</basedir>
<logdir>/var/log/icecast2</logdir>
<webroot>/usr/share/icecast2/web</webroot>
<adminroot>/usr/share/icecast2/admin</adminroot>
</paths>
<logging>
<accesslog>access.log</accesslog>
<errorlog>error.log</errorlog>
<loglevel>3</loglevel>
<logsize>10000</logsize>
</logging>
<security>
<chroot>0</chroot>
</security>
<listen-socket>
<bind-address>0.0.0.0</bind-address>
<port>8000</port>
<shoutcast-compat>0</shoutcast-compat>
</listen-socket>
<relay>
<server>example.com</server>
<port>80</port>
<mount>/stream2.ogg</mount>
<local-mount>/_stream/stream2.ogg</local-mount>
<username></username>
<password></password>
<on-demand>1</on-demand>
<relay-shoutcast-metadata>1</relay-shoutcast-metadata>
</relay>
<mount>
<mount-name>/_stream/stream1.ogg</mount-name>
<username>user</username>
<password>pass</password>
<fallback-mount>/fallback.ogg</fallback-mount>
<fallback-override>1</fallback-override>
<hidden>0</hidden>
</mount>
<mount>
<mount-name>/_stream/stream3.mp3</mount-name>
<username>user</username>
<password>pass</password>
<hidden>0</hidden>
</mount>
</icecast>`
)
// Create an icecastConfig object with known passwords.
func createTestIcecastConfig() *icecastConfig {
ic := newIcecastConfig("1.2.3.4", 1000)
ic.Auth.SourcePassword = "sourcepass"
ic.Auth.AdminPassword = "adminpass"
return ic
}
func checkStrings(t *testing.T, got, want string) {
if got == want {
return
}
var diffout bytes.Buffer
for _, d := range difflib.Diff(strings.Split(want, "\n"), strings.Split(got, "\n")) {
fmt.Fprintf(&diffout, "%s\n", d.String())
}
t.Errorf("unexpected result: (+ got, - want)\n\n%s\n", diffout.String())
}
func TestIcecastConfig(t *testing.T) {
mount := &autoradio.Mount{
Name: "/test.ogg",
// Create a test config with a few different mount types.
c := newClusterConfig()
c.setMountIfChanged(&autoradio.Mount{
Name: "/stream1.ogg",
Username: "user",
Password: "pass",
}
c := newClusterConfig()
c.setMountIfChanged(mount)
Fallback: "/fallback.ogg",
})
c.setMountIfChanged(&autoradio.Mount{
Name: "/stream2.ogg",
RelayUrl: "http://example.com/stream2.ogg",
})
c.setMountIfChanged(&autoradio.Mount{
Name: "/stream3.mp3",
Username: "user",
Password: "pass",
Transcoding: &autoradio.EncodingParams{
SourceName: "/stream2.ogg",
Format: "mp3",
BitRate: 64,
SampleRate: 44100,
Channels: 2,
},
})
// Test a relay config.
ice := newIcecastConfig("1.2.3.4", 1000)
ice := createTestIcecastConfig()
ice.Update(c, false, "2.3.4.5")
output, err := ice.Encode()
if err != nil {
t.Fatal(err)
}
outputs := string(output)
if !strings.Contains(outputs, "<icecast>") {
t.Fatalf("No <icecast> element:\n%s", output)
}
if !strings.Contains(outputs, "<relay>") {
t.Fatalf("Mount not configured as relay:\n%s", output)
}
checkStrings(t, string(output), expectedSlaveConfig)
// Test a master config.
ice = newIcecastConfig("1.2.3.4", 1000)
ice = createTestIcecastConfig()
ice.Update(c, true, "2.3.4.5")
output, err = ice.Encode()
if err != nil {
t.Fatal(err)
}
outputs = string(output)
if !strings.Contains(outputs, "<mount>") {
t.Fatalf("Mount not configured as master:\n%s", output)
}
checkStrings(t, string(output), expectedMasterConfig)
}
......@@ -406,11 +406,12 @@ func (rc *RadioNode) updater(stop chan bool) {
continue
}
rc.Log.Printf("updating configuration")
masterAddr := rc.getMasterAddr()
// Reload the Icecast daemon.
icecastReloads.Incr()
rc.Log.Printf("reloading icecast config")
if err := rc.icecast.Update(rc.config, rc.me.IsMaster(), masterAddr); err != nil {
icecastReloadErrors.Incr()
rc.Log.Printf("Update(): %v", err)
......
package util
import (
"bytes"
"io/ioutil"
"os"
"path/filepath"
)
// WriteFileIfChanged updates the contents of a file if they have
// changed. It returns false if the file already exists and its
// contents are equal to 'data', true in any other case.
func WriteFileIfChanged(path string, data []byte) (bool, error) {
if cur, err := ioutil.ReadFile(path); err == nil && bytes.Equal(cur, data) {
return false, nil
}
tmpf, err := ioutil.TempFile(filepath.Dir(path), ".tmp")
if err != nil {
return true, err
}
defer os.Remove(tmpf.Name())
tmpf.Write(data)
tmpf.Close()
os.Chmod(tmpf.Name(), 0644)
return true, os.Rename(tmpf.Name(), path)
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment