syncthing/main.go

462 lines
10 KiB
Go
Raw Normal View History

2013-12-15 10:43:31 +00:00
package main
import (
2014-01-01 21:31:04 +00:00
"compress/gzip"
2013-12-15 10:43:31 +00:00
"crypto/tls"
2014-01-26 13:28:41 +00:00
"flag"
2014-01-08 13:37:33 +00:00
"fmt"
2013-12-15 10:43:31 +00:00
"log"
"net"
"net/http"
_ "net/http/pprof"
"os"
"path"
2014-01-09 23:09:27 +00:00
"runtime"
"runtime/debug"
2013-12-15 10:43:31 +00:00
"strconv"
"strings"
"time"
"github.com/calmh/ini"
"github.com/calmh/syncthing/discover"
"github.com/calmh/syncthing/model"
2013-12-15 10:43:31 +00:00
"github.com/calmh/syncthing/protocol"
)
2013-12-18 18:36:28 +00:00
var opts Options
2013-12-30 15:05:27 +00:00
var Version string = "unknown-dev"
2013-12-18 18:36:28 +00:00
2013-12-15 10:43:31 +00:00
const (
confFileName = "syncthing.ini"
)
var (
2014-01-12 22:19:03 +00:00
myID string
2013-12-15 10:43:31 +00:00
config ini.Config
nodeAddrs = make(map[string][]string)
)
2014-01-26 13:28:41 +00:00
var (
showVersion bool
showConfig bool
confDir string
trace string
profiler string
)
2013-12-15 10:43:31 +00:00
func main() {
2014-01-22 11:51:49 +00:00
log.SetOutput(os.Stderr)
logger = log.New(os.Stderr, "", log.Flags())
2014-01-26 13:28:41 +00:00
flag.StringVar(&confDir, "home", "~/.syncthing", "Set configuration directory")
flag.BoolVar(&showConfig, "config", false, "Print current configuration")
flag.StringVar(&trace, "debug.trace", "", "(connect,net,idx,file,pull)")
flag.StringVar(&profiler, "debug.profiler", "", "(addr)")
flag.BoolVar(&showVersion, "version", false, "Show version")
flag.Usage = usageFor(flag.CommandLine, "syncthing [options]")
flag.Parse()
2014-01-08 13:37:33 +00:00
2014-01-26 13:28:41 +00:00
if showVersion {
2014-01-08 13:37:33 +00:00
fmt.Println(Version)
2013-12-18 18:36:28 +00:00
os.Exit(0)
2013-12-15 10:43:31 +00:00
}
2014-01-08 13:37:33 +00:00
2014-01-09 23:09:27 +00:00
if len(os.Getenv("GOGC")) == 0 {
debug.SetGCPercent(25)
}
if len(os.Getenv("GOMAXPROCS")) == 0 {
runtime.GOMAXPROCS(runtime.NumCPU())
}
2014-01-26 13:28:41 +00:00
if len(trace) > 0 {
2014-01-13 17:29:23 +00:00
log.SetFlags(log.Lshortfile | log.Ldate | log.Ltime | log.Lmicroseconds)
logger.SetFlags(log.Lshortfile | log.Ldate | log.Ltime | log.Lmicroseconds)
2013-12-31 02:21:31 +00:00
}
2014-01-26 13:28:41 +00:00
confDir = expandTilde(confDir)
2013-12-21 23:16:49 +00:00
2013-12-15 10:43:31 +00:00
// Ensure that our home directory exists and that we have a certificate and key.
2014-01-26 13:28:41 +00:00
ensureDir(confDir, 0700)
cert, err := loadCert(confDir)
2013-12-15 10:43:31 +00:00
if err != nil {
2014-01-26 13:28:41 +00:00
newCertificate(confDir)
cert, err = loadCert(confDir)
2013-12-15 10:43:31 +00:00
fatalErr(err)
}
2014-01-12 22:19:03 +00:00
myID = string(certId(cert.Certificate[0]))
2014-01-20 21:22:27 +00:00
log.SetPrefix("[" + myID[0:5] + "] ")
logger.SetPrefix("[" + myID[0:5] + "] ")
2013-12-15 10:43:31 +00:00
// Load the configuration file, if it exists.
// If it does not, create a template.
2014-01-26 13:28:41 +00:00
cfgFile := path.Join(confDir, confFileName)
cf, err := os.Open(cfgFile)
if err != nil {
2014-01-26 13:28:41 +00:00
infoln("My ID:", myID)
infoln("No config file; creating a template")
2014-01-26 13:28:41 +00:00
loadConfig(nil, &opts) //loads defaults
fd, err := os.Create(cfgFile)
if err != nil {
fatalln(err)
}
2014-01-26 13:28:41 +00:00
writeConfig(fd, "~/Sync", map[string]string{myID: "dynamic"}, opts, true)
fd.Close()
infof("Edit %s to suit and restart syncthing.", cfgFile)
2014-01-26 13:28:41 +00:00
os.Exit(0)
}
config = ini.Parse(cf)
cf.Close()
2014-01-26 13:28:41 +00:00
loadConfig(config.OptionMap("settings"), &opts)
if showConfig {
writeConfig(os.Stdout,
config.Get("repository", "dir"),
config.OptionMap("nodes"), opts, false)
os.Exit(0)
}
infoln("Version", Version)
infoln("My ID:", myID)
var dir = expandTilde(config.Get("repository", "dir"))
if len(dir) == 0 {
fatalln("No repository directory. Set dir under [repository] in syncthing.ini.")
}
2014-01-26 13:28:41 +00:00
if len(profiler) > 0 {
2013-12-15 10:43:31 +00:00
go func() {
2014-01-26 13:28:41 +00:00
err := http.ListenAndServe(profiler, nil)
2013-12-18 18:36:28 +00:00
if err != nil {
warnln(err)
}
2013-12-15 10:43:31 +00:00
}()
}
// The TLS configuration is used for both the listening socket and outgoing
// connections.
cfg := &tls.Config{
2014-01-09 08:28:08 +00: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 10:43:31 +00:00
}
// Create a map of desired node connections based on the configuration file
// directives.
for nodeID, addrs := range config.OptionMap("nodes") {
addrs := strings.Fields(addrs)
nodeAddrs[nodeID] = addrs
}
2013-12-21 23:16:49 +00:00
ensureDir(dir, -1)
2014-01-26 13:28:41 +00:00
m := model.NewModel(dir, opts.MaxChangeBW*1000)
for _, t := range strings.Split(trace, ",") {
m.Trace(t)
}
2014-01-26 13:28:41 +00:00
if opts.LimitRate > 0 {
m.LimitRate(opts.LimitRate)
2014-01-12 15:59:35 +00:00
}
2013-12-15 10:43:31 +00:00
2014-01-05 22:54:57 +00:00
// GUI
2014-01-26 13:28:41 +00:00
if opts.GUI && opts.GUIAddr != "" {
host, port, err := net.SplitHostPort(opts.GUIAddr)
if err != nil {
warnf("Cannot start GUI on %q: %v", opts.GUIAddr, err)
} else {
if len(host) > 0 {
infof("Starting web GUI on http://%s", opts.GUIAddr)
} else {
infof("Starting web GUI on port %s", port)
}
startGUI(opts.GUIAddr, m)
}
2014-01-05 22:54:57 +00:00
}
2013-12-15 10:43:31 +00:00
// Walk the repository and update the local model before establishing any
// connections to other nodes.
infoln("Populating repository index")
2013-12-15 10:43:31 +00:00
updateLocalModel(m)
// Routine to listen for incoming connections
infoln("Listening for incoming connections")
2013-12-18 18:36:28 +00:00
go listen(myID, opts.Listen, m, cfg)
2013-12-15 10:43:31 +00:00
// Routine to connect out to configured nodes
infoln("Attempting to connect to other nodes")
2013-12-18 18:36:28 +00:00
go connect(myID, opts.Listen, nodeAddrs, m, cfg)
2013-12-15 10:43:31 +00:00
// Routine to pull blocks from other nodes to synchronize the local
// repository. Does not run when we are in read only (publish only) mode.
2013-12-18 18:36:28 +00:00
if !opts.ReadOnly {
2014-01-26 13:28:41 +00:00
if opts.Delete {
2014-01-07 15:15:18 +00:00
infoln("Deletes from peer nodes are allowed")
2014-01-26 13:28:41 +00:00
} else {
infoln("Deletes from peer nodes will be ignored")
}
okln("Ready to synchronize (read-write)")
2014-01-26 13:28:41 +00:00
m.StartRW(opts.Delete, opts.RequestsInFlight)
} else {
okln("Ready to synchronize (read only; no external updates accepted)")
2013-12-15 10:43:31 +00:00
}
// Periodically scan the repository and update the local model.
// XXX: Should use some fsnotify mechanism.
go func() {
for {
2014-01-26 13:28:41 +00:00
time.Sleep(opts.ScanInterval)
if m.LocalAge() > opts.ScanInterval.Seconds()/2 {
2014-01-20 21:22:27 +00:00
updateLocalModel(m)
}
2013-12-15 10:43:31 +00:00
}
}()
2014-01-26 13:28:41 +00:00
// Periodically print statistics
go printStatsLoop(m)
2014-01-05 15:16:37 +00:00
2013-12-15 10:43:31 +00:00
select {}
}
func printStatsLoop(m *model.Model) {
2014-01-05 15:16:37 +00:00
var lastUpdated int64
var lastStats = make(map[string]model.ConnectionInfo)
2014-01-05 15:16:37 +00:00
for {
time.Sleep(60 * time.Second)
for node, stats := range m.ConnectionStats() {
secs := time.Since(lastStats[node].At).Seconds()
inbps := 8 * int(float64(stats.InBytesTotal-lastStats[node].InBytesTotal)/secs)
outbps := 8 * int(float64(stats.OutBytesTotal-lastStats[node].OutBytesTotal)/secs)
if inbps+outbps > 0 {
2014-01-20 21:22:27 +00:00
infof("%s: %sb/s in, %sb/s out", node[0:5], MetricPrefix(inbps), MetricPrefix(outbps))
2014-01-05 15:16:37 +00:00
}
lastStats[node] = stats
}
if lu := m.Generation(); lu > lastUpdated {
lastUpdated = lu
2014-01-05 22:54:57 +00:00
files, _, bytes := m.GlobalSize()
2014-01-05 15:16:37 +00:00
infof("%6d files, %9sB in cluster", files, BinaryPrefix(bytes))
2014-01-05 22:54:57 +00:00
files, _, bytes = m.LocalSize()
2014-01-05 15:16:37 +00:00
infof("%6d files, %9sB in local repo", files, BinaryPrefix(bytes))
2014-01-05 22:54:57 +00:00
needFiles, bytes := m.NeedFiles()
infof("%6d files, %9sB to synchronize", len(needFiles), BinaryPrefix(bytes))
2014-01-05 15:16:37 +00:00
}
}
}
func listen(myID string, addr string, m *model.Model, cfg *tls.Config) {
2013-12-15 10:43:31 +00:00
l, err := tls.Listen("tcp", addr, cfg)
fatalErr(err)
2014-01-23 12:12:45 +00:00
connOpts := map[string]string{
"clientId": "syncthing",
"clientVersion": Version,
}
2013-12-15 10:43:31 +00:00
listen:
for {
conn, err := l.Accept()
if err != nil {
warnln(err)
continue
}
2014-01-26 13:28:41 +00:00
if strings.Contains(trace, "connect") {
2013-12-15 10:43:31 +00:00
debugln("NET: Connect from", conn.RemoteAddr())
}
tc := conn.(*tls.Conn)
err = tc.Handshake()
if err != nil {
warnln(err)
tc.Close()
continue
}
remoteID := certId(tc.ConnectionState().PeerCertificates[0].Raw)
if remoteID == myID {
warnf("Connect from myself (%s) - should not happen", remoteID)
conn.Close()
continue
}
if m.ConnectedTo(remoteID) {
warnf("Connect from connected node (%s)", remoteID)
}
for nodeID := range nodeAddrs {
if nodeID == remoteID {
2014-01-23 12:12:45 +00:00
protoConn := protocol.NewConnection(remoteID, conn, conn, m, connOpts)
2014-01-09 12:58:35 +00:00
m.AddConnection(conn, protoConn)
2013-12-15 10:43:31 +00:00
continue listen
}
}
conn.Close()
}
}
func connect(myID string, addr string, nodeAddrs map[string][]string, m *model.Model, cfg *tls.Config) {
2013-12-15 10:43:31 +00:00
_, portstr, err := net.SplitHostPort(addr)
fatalErr(err)
port, _ := strconv.Atoi(portstr)
2014-01-26 13:28:41 +00:00
if !opts.LocalDiscovery {
2013-12-22 21:29:23 +00:00
port = -1
} else {
infoln("Sending local discovery announcements")
}
2014-01-26 13:28:41 +00:00
if !opts.ExternalDiscovery {
opts.ExternalServer = ""
2013-12-22 21:29:23 +00:00
} else {
infoln("Sending external discovery announcements")
}
2014-01-26 13:28:41 +00:00
disc, err := discover.NewDiscoverer(myID, port, opts.ExternalServer)
2013-12-22 21:29:23 +00:00
2013-12-15 10:43:31 +00:00
if err != nil {
2013-12-22 21:29:23 +00:00
warnf("No discovery possible (%v)", err)
2013-12-15 10:43:31 +00:00
}
2014-01-23 12:12:45 +00:00
connOpts := map[string]string{
"clientId": "syncthing",
"clientVersion": Version,
}
2013-12-15 10:43:31 +00:00
for {
nextNode:
for nodeID, addrs := range nodeAddrs {
if nodeID == myID {
continue
}
if m.ConnectedTo(nodeID) {
continue
}
for _, addr := range addrs {
if addr == "dynamic" {
var ok bool
if disc != nil {
addr, ok = disc.Lookup(nodeID)
}
if !ok {
continue
}
}
2014-01-26 13:28:41 +00:00
if strings.Contains(trace, "connect") {
2013-12-15 10:43:31 +00:00
debugln("NET: Dial", nodeID, addr)
}
conn, err := tls.Dial("tcp", addr, cfg)
if err != nil {
2014-01-26 13:28:41 +00:00
if strings.Contains(trace, "connect") {
2013-12-15 10:43:31 +00:00
debugln("NET:", err)
}
continue
}
remoteID := certId(conn.ConnectionState().PeerCertificates[0].Raw)
if remoteID != nodeID {
warnln("Unexpected nodeID", remoteID, "!=", nodeID)
conn.Close()
continue
}
2014-01-23 12:12:45 +00:00
protoConn := protocol.NewConnection(remoteID, conn, conn, m, connOpts)
2014-01-09 12:58:35 +00:00
m.AddConnection(conn, protoConn)
2013-12-15 10:43:31 +00:00
continue nextNode
}
}
2014-01-26 13:28:41 +00:00
time.Sleep(opts.ConnInterval)
2013-12-15 10:43:31 +00:00
}
}
func updateLocalModel(m *model.Model) {
2014-01-26 13:28:41 +00:00
files, _ := m.Walk(opts.Symlinks)
2013-12-15 10:43:31 +00:00
m.ReplaceLocal(files)
saveIndex(m)
}
func saveIndex(m *model.Model) {
name := m.RepoID() + ".idx.gz"
2014-01-26 13:28:41 +00:00
fullName := path.Join(confDir, name)
2013-12-31 03:04:30 +00:00
idxf, err := os.Create(fullName + ".tmp")
2013-12-15 10:43:31 +00:00
if err != nil {
return
}
2014-01-01 21:31:04 +00:00
gzw := gzip.NewWriter(idxf)
protocol.WriteIndex(gzw, m.ProtocolIndex())
gzw.Close()
2013-12-15 10:43:31 +00:00
idxf.Close()
2013-12-31 03:04:30 +00:00
os.Rename(fullName+".tmp", fullName)
2013-12-15 10:43:31 +00:00
}
func loadIndex(m *model.Model) {
name := m.RepoID() + ".idx.gz"
2014-01-26 13:28:41 +00:00
idxf, err := os.Open(path.Join(confDir, name))
2013-12-15 10:43:31 +00:00
if err != nil {
return
}
defer idxf.Close()
2014-01-01 21:31:04 +00:00
gzr, err := gzip.NewReader(idxf)
if err != nil {
return
}
defer gzr.Close()
idx, err := protocol.ReadIndex(gzr)
2013-12-15 10:43:31 +00:00
if err != nil {
return
}
m.SeedLocal(idx)
2013-12-15 10:43:31 +00:00
}
2013-12-21 23:16:49 +00:00
func ensureDir(dir string, mode int) {
2013-12-15 10:43:31 +00:00
fi, err := os.Stat(dir)
if os.IsNotExist(err) {
err := os.MkdirAll(dir, 0700)
fatalErr(err)
2013-12-21 23:16:49 +00:00
} else if mode >= 0 && err == nil && int(fi.Mode()&0777) != mode {
err := os.Chmod(dir, os.FileMode(mode))
2013-12-15 10:43:31 +00:00
fatalErr(err)
}
}
func expandTilde(p string) string {
if strings.HasPrefix(p, "~/") {
return strings.Replace(p, "~", getHomeDir(), 1)
}
return p
}
2013-12-15 10:43:31 +00:00
func getHomeDir() string {
home := os.Getenv("HOME")
if home == "" {
fatalln("No home directory?")
}
return home
}