syncthing/cmd/syncthing/main.go

513 lines
12 KiB
Go
Raw Normal View History

2013-12-15 11:43:31 +01:00
package main
import (
"crypto/tls"
2014-01-26 14:28:41 +01:00
"flag"
2014-01-08 14:37:33 +01:00
"fmt"
2014-04-01 20:36:54 +02:00
"io"
2013-12-15 11:43:31 +01:00
"log"
"net"
"net/http"
_ "net/http/pprof"
"os"
"os/exec"
"path/filepath"
2014-01-10 00:09:27 +01:00
"runtime"
"runtime/debug"
2013-12-15 11:43:31 +01:00
"strings"
"time"
2013-12-15 11:43:31 +01:00
"github.com/calmh/syncthing/discover"
"github.com/calmh/syncthing/protocol"
2014-04-01 20:36:54 +02:00
"github.com/juju/ratelimit"
2013-12-15 11:43:31 +01:00
)
const BlockSize = 128 * 1024
var cfg Configuration
2014-02-24 13:29:30 +01:00
var Version = "unknown-dev"
2013-12-18 19:36:28 +01:00
2013-12-15 11:43:31 +01:00
var (
2014-04-01 20:36:54 +02:00
myID string
confDir string
rateBucket *ratelimit.Bucket
2014-01-26 14:28:41 +01:00
)
const (
usage = "syncthing [options]"
extraUsage = `The following enviroment variables are interpreted by syncthing:
STNORESTART Do not attempt to restart when requested to, instead just exit.
Set this variable when running under a service manager such as
runit, launchd, etc.
STPROFILER Set to a listen address such as "127.0.0.1:9090" to start the
profiler with HTTP access.
STTRACE A comma separated string of facilities to trace. The valid
facility strings:
- "discover" (the node discovery package)
- "files" (file set store)
- "idx" (index sending and receiving)
- "mc" (multicast beacon)
- "need" (file need calculations)
- "net" (connecting and disconnecting, network messages)
- "pull" (file pull activity)
- "scanner" (the file change scanner)
`
)
2013-12-15 11:43:31 +01:00
func main() {
var reset bool
2014-04-01 20:36:54 +02:00
var showVersion bool
flag.StringVar(&confDir, "home", getDefaultConfDir(), "Set configuration directory")
flag.BoolVar(&reset, "reset", false, "Prepare to resync from cluster")
2014-01-26 14:28:41 +01:00
flag.BoolVar(&showVersion, "version", false, "Show version")
flag.Usage = usageFor(flag.CommandLine, usage, extraUsage)
2014-01-26 14:28:41 +01:00
flag.Parse()
2014-01-08 14:37:33 +01:00
2014-03-09 08:35:38 +01:00
if len(os.Getenv("STRESTART")) > 0 {
// Give the parent process time to exit and release sockets etc.
time.Sleep(1 * time.Second)
2014-02-12 12:10:44 +01:00
}
2014-01-26 14:28:41 +01:00
if showVersion {
2014-03-31 06:51:01 +02:00
fmt.Printf("syncthing %s (%s %s-%s)\n", Version, runtime.Version(), runtime.GOOS, runtime.GOARCH)
2013-12-18 19:36:28 +01:00
os.Exit(0)
2013-12-15 11:43:31 +01:00
}
2014-01-08 14:37:33 +01:00
2014-01-10 00:09:27 +01:00
if len(os.Getenv("GOGC")) == 0 {
debug.SetGCPercent(25)
}
if len(os.Getenv("GOMAXPROCS")) == 0 {
runtime.GOMAXPROCS(runtime.NumCPU())
}
2014-01-26 14:28:41 +01:00
confDir = expandTilde(confDir)
2013-12-22 00:16:49 +01:00
2013-12-15 11:43:31 +01:00
// Ensure that our home directory exists and that we have a certificate and key.
2014-01-26 14:28:41 +01:00
ensureDir(confDir, 0700)
cert, err := loadCert(confDir)
2013-12-15 11:43:31 +01:00
if err != nil {
2014-01-26 14:28:41 +01:00
newCertificate(confDir)
cert, err = loadCert(confDir)
2013-12-15 11:43:31 +01:00
fatalErr(err)
}
2014-02-24 13:29:30 +01:00
myID = string(certID(cert.Certificate[0]))
2014-01-20 22:22:27 +01:00
log.SetPrefix("[" + myID[0:5] + "] ")
logger.SetPrefix("[" + myID[0:5] + "] ")
2013-12-15 11:43:31 +01:00
infoln("Version", Version)
infoln("My ID:", myID)
// Prepare to be able to save configuration
cfgFile := filepath.Join(confDir, "config.xml")
go saveConfigLoop(cfgFile)
// Load the configuration file, if it exists.
// If it does not, create a template.
cf, err := os.Open(cfgFile)
if err == nil {
// Read config.xml
cfg, err = readConfigXML(cf)
if err != nil {
fatalln(err)
}
cf.Close()
}
if len(cfg.Repositories) == 0 {
infoln("No config file; starting with empty defaults")
cfg, err = readConfigXML(nil)
cfg.Repositories = []RepositoryConfiguration{
{
ID: "default",
Directory: filepath.Join(getHomeDir(), "Sync"),
Nodes: []NodeConfiguration{{NodeID: myID}},
},
}
cfg.Nodes = []NodeConfiguration{
{NodeID: myID, Addresses: []string{"dynamic"}},
}
saveConfig()
infof("Edit %s to taste or use the GUI\n", cfgFile)
}
2014-01-26 14:28:41 +01:00
if reset {
resetRepositories()
os.Exit(0)
}
if profiler := os.Getenv("STPROFILER"); len(profiler) > 0 {
2013-12-15 11:43:31 +01:00
go func() {
2014-03-09 09:18:28 +01:00
dlog.Println("Starting profiler on", profiler)
2014-01-26 14:28:41 +01:00
err := http.ListenAndServe(profiler, nil)
2013-12-18 19:36:28 +01:00
if err != nil {
2014-03-09 09:18:28 +01:00
dlog.Fatal(err)
2013-12-18 19:36:28 +01:00
}
2013-12-15 11:43:31 +01:00
}()
}
// The TLS configuration is used for both the listening socket and outgoing
// connections.
tlsCfg := &tls.Config{
2014-01-09 09:28:08 +01:00
Certificates: []tls.Certificate{cert},
NextProtos: []string{"bep/1.0"},
ServerName: myID,
ClientAuth: tls.RequestClientCert,
SessionTicketsDisabled: true,
InsecureSkipVerify: true,
MinVersion: tls.VersionTLS12,
2013-12-15 11:43:31 +01:00
}
2014-04-01 20:36:54 +02:00
// If the write rate should be limited, set up a rate limiter for it.
// This will be used on connections created in the connect and listen routines.
if cfg.Options.MaxSendKbps > 0 {
2014-04-01 20:36:54 +02:00
rateBucket = ratelimit.NewBucketWithRate(float64(1000*cfg.Options.MaxSendKbps), int64(5*1000*cfg.Options.MaxSendKbps))
2014-01-12 16:59:35 +01:00
}
2013-12-15 11:43:31 +01:00
2014-04-01 20:36:54 +02:00
m := NewModel(cfg.Options.MaxChangeKbps * 1000)
for i := range cfg.Repositories {
cfg.Repositories[i].Nodes = cleanNodeList(cfg.Repositories[i].Nodes, myID)
dir := expandTilde(cfg.Repositories[i].Directory)
ensureDir(dir, -1)
m.AddRepo(cfg.Repositories[i].ID, dir, cfg.Repositories[i].Nodes)
}
2014-01-05 23:54:57 +01:00
// GUI
2014-04-08 15:56:12 +02:00
if cfg.GUI.Enabled && cfg.GUI.Address != "" {
addr, err := net.ResolveTCPAddr("tcp", cfg.GUI.Address)
if err != nil {
2014-04-08 15:56:12 +02:00
warnf("Cannot start GUI on %q: %v", cfg.GUI.Address, err)
} else {
2014-03-02 12:52:32 +01:00
var hostOpen, hostShow string
switch {
case addr.IP == nil:
hostOpen = "localhost"
hostShow = "0.0.0.0"
case addr.IP.IsUnspecified():
hostOpen = "localhost"
hostShow = addr.IP.String()
default:
hostOpen = addr.IP.String()
hostShow = hostOpen
}
2014-03-02 12:52:32 +01:00
infof("Starting web GUI on http://%s:%d/", hostShow, addr.Port)
2014-04-08 15:56:12 +02:00
startGUI(cfg.GUI, m)
2014-03-09 08:35:53 +01:00
if cfg.Options.StartBrowser && len(os.Getenv("STRESTART")) == 0 {
openURL(fmt.Sprintf("http://%s:%d", hostOpen, addr.Port))
}
}
2014-01-05 23:54:57 +01:00
}
2013-12-15 11:43:31 +01:00
// Walk the repository and update the local model before establishing any
// connections to other nodes.
2014-03-31 06:47:08 +02:00
infoln("Populating repository index")
m.LoadIndexes(confDir)
m.ScanRepos()
m.SaveIndexes(confDir)
2013-12-15 11:43:31 +01:00
// Routine to connect out to configured nodes
disc := discovery()
go listenConnect(myID, disc, m, tlsCfg)
2013-12-15 11:43:31 +01:00
for _, repo := range cfg.Repositories {
// Routine to pull blocks from other nodes to synchronize the local
// repository. Does not run when we are in read only (publish only) mode.
if repo.ReadOnly {
okf("Ready to synchronize %s (read only; no external updates accepted)", repo.ID)
m.StartRepoRO(repo.ID)
} else {
okf("Ready to synchronize %s (read-write)", repo.ID)
m.StartRepoRW(repo.ID, cfg.Options.ParallelRequests)
}
}
2014-01-05 16:16:37 +01:00
2013-12-15 11:43:31 +01:00
select {}
}
func resetRepositories() {
suffix := fmt.Sprintf(".syncthing-reset-%d", time.Now().UnixNano())
for _, repo := range cfg.Repositories {
if _, err := os.Stat(repo.Directory); err == nil {
infof("Reset: Moving %s -> %s", repo.Directory, repo.Directory+suffix)
os.Rename(repo.Directory, repo.Directory+suffix)
}
}
pat := filepath.Join(confDir, "*.idx.gz")
idxs, err := filepath.Glob(pat)
if err == nil {
for _, idx := range idxs {
infof("Reset: Removing %s", idx)
os.Remove(idx)
}
}
}
2014-02-12 12:10:44 +01:00
func restart() {
infoln("Restarting")
if os.Getenv("SMF_FMRI") != "" || os.Getenv("STNORESTART") != "" {
// Solaris SMF
infoln("Service manager detected; exit instead of restart")
os.Exit(0)
}
2014-03-09 08:35:38 +01:00
env := os.Environ()
if len(os.Getenv("STRESTART")) == 0 {
env = append(env, "STRESTART=1")
2014-02-12 12:10:44 +01:00
}
pgm, err := exec.LookPath(os.Args[0])
if err != nil {
warnln(err)
return
}
2014-03-09 08:35:38 +01:00
proc, err := os.StartProcess(pgm, os.Args, &os.ProcAttr{
Env: env,
2014-02-12 12:10:44 +01:00
Files: []*os.File{os.Stdin, os.Stdout, os.Stderr},
})
if err != nil {
fatalln(err)
}
proc.Release()
os.Exit(0)
}
var saveConfigCh = make(chan struct{})
func saveConfigLoop(cfgFile string) {
for _ = range saveConfigCh {
fd, err := os.Create(cfgFile + ".tmp")
if err != nil {
warnln(err)
continue
}
err = writeConfigXML(fd, cfg)
if err != nil {
warnln(err)
fd.Close()
continue
}
err = fd.Close()
if err != nil {
warnln(err)
continue
}
err = Rename(cfgFile+".tmp", cfgFile)
if err != nil {
warnln(err)
}
}
}
func saveConfig() {
saveConfigCh <- struct{}{}
}
func listenConnect(myID string, disc *discover.Discoverer, m *Model, tlsCfg *tls.Config) {
var conns = make(chan *tls.Conn)
// Listen
for _, addr := range cfg.Options.ListenAddress {
addr := addr
go func() {
if debugNet {
dlog.Println("listening on", addr)
}
l, err := tls.Listen("tcp", addr, tlsCfg)
fatalErr(err)
for {
conn, err := l.Accept()
if err != nil {
warnln(err)
continue
}
if debugNet {
dlog.Println("connect from", conn.RemoteAddr())
}
tc := conn.(*tls.Conn)
err = tc.Handshake()
if err != nil {
warnln(err)
tc.Close()
continue
}
conns <- tc
}
}()
}
2013-12-15 11:43:31 +01:00
// Connect
go func() {
for {
nextNode:
for _, nodeCfg := range cfg.Nodes {
if nodeCfg.NodeID == myID {
continue
}
if m.ConnectedTo(nodeCfg.NodeID) {
continue
}
var addrs []string
for _, addr := range nodeCfg.Addresses {
if addr == "dynamic" {
if disc != nil {
t := disc.Lookup(nodeCfg.NodeID)
if len(t) == 0 {
continue
}
addrs = append(addrs, t...)
}
} else {
addrs = append(addrs, addr)
}
}
2013-12-15 11:43:31 +01:00
for _, addr := range addrs {
if debugNet {
dlog.Println("dial", nodeCfg.NodeID, addr)
}
conn, err := tls.Dial("tcp", addr, tlsCfg)
if err != nil {
if debugNet {
dlog.Println(err)
}
continue
}
2013-12-15 11:43:31 +01:00
conns <- conn
continue nextNode
}
}
time.Sleep(time.Duration(cfg.Options.ReconnectIntervalS) * time.Second)
2013-12-15 11:43:31 +01:00
}
}()
2013-12-15 11:43:31 +01:00
next:
for conn := range conns {
certs := conn.ConnectionState().PeerCertificates
if l := len(certs); l != 1 {
warnf("Got peer certificate list of length %d != 1; protocol error", l)
conn.Close()
continue
}
remoteID := certID(certs[0].Raw)
2013-12-15 11:43:31 +01:00
if remoteID == myID {
warnf("Connected to myself (%s) - should not happen", remoteID)
2013-12-15 11:43:31 +01:00
conn.Close()
continue
}
if m.ConnectedTo(remoteID) {
warnf("Connected to already connected node (%s)", remoteID)
conn.Close()
continue
2013-12-15 11:43:31 +01:00
}
for _, nodeCfg := range cfg.Nodes {
if nodeCfg.NodeID == remoteID {
2014-04-01 20:36:54 +02:00
var wr io.Writer = conn
if rateBucket != nil {
wr = &limitedWriter{conn, rateBucket}
}
protoConn := protocol.NewConnection(remoteID, conn, wr, m)
2014-01-09 13:58:35 +01:00
m.AddConnection(conn, protoConn)
continue next
2013-12-15 11:43:31 +01:00
}
}
conn.Close()
}
}
func discovery() *discover.Discoverer {
if !cfg.Options.LocalAnnEnabled {
return nil
2013-12-22 22:29:23 +01:00
}
infoln("Sending local discovery announcements")
if !cfg.Options.GlobalAnnEnabled {
cfg.Options.GlobalAnnServer = ""
2014-03-31 06:47:08 +02:00
} else {
2013-12-22 22:29:23 +01:00
infoln("Sending external discovery announcements")
}
disc, err := discover.NewDiscoverer(myID, cfg.Options.ListenAddress, cfg.Options.GlobalAnnServer)
2013-12-22 22:29:23 +01:00
2013-12-15 11:43:31 +01:00
if err != nil {
2013-12-22 22:29:23 +01:00
warnf("No discovery possible (%v)", err)
2013-12-15 11:43:31 +01:00
}
return disc
}
2013-12-22 00:16:49 +01:00
func ensureDir(dir string, mode int) {
2013-12-15 11:43:31 +01:00
fi, err := os.Stat(dir)
if os.IsNotExist(err) {
err := os.MkdirAll(dir, 0700)
fatalErr(err)
2013-12-22 00:16:49 +01:00
} else if mode >= 0 && err == nil && int(fi.Mode()&0777) != mode {
err := os.Chmod(dir, os.FileMode(mode))
2013-12-15 11:43:31 +01:00
fatalErr(err)
}
}
func expandTilde(p string) string {
if runtime.GOOS == "windows" {
return p
}
if strings.HasPrefix(p, "~/") {
return strings.Replace(p, "~", getUnixHomeDir(), 1)
}
return p
}
func getUnixHomeDir() string {
home := os.Getenv("HOME")
if home == "" {
fatalln("No home directory?")
}
return home
}
2013-12-15 11:43:31 +01:00
func getHomeDir() string {
if runtime.GOOS == "windows" {
home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH")
if home == "" {
home = os.Getenv("USERPROFILE")
}
return home
}
return getUnixHomeDir()
}
func getDefaultConfDir() string {
if runtime.GOOS == "windows" {
return filepath.Join(os.Getenv("AppData"), "syncthing")
2013-12-15 11:43:31 +01:00
}
return expandTilde("~/.syncthing")
2013-12-15 11:43:31 +01:00
}