Source file src/cmd/vendor/github.com/google/pprof/internal/driver/fetch.go

     1  // Copyright 2014 Google Inc. All Rights Reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package driver
    16  
    17  import (
    18  	"bytes"
    19  	"fmt"
    20  	"io"
    21  	"net/http"
    22  	"net/url"
    23  	"os"
    24  	"os/exec"
    25  	"path/filepath"
    26  	"runtime"
    27  	"strconv"
    28  	"strings"
    29  	"sync"
    30  	"time"
    31  
    32  	"github.com/google/pprof/internal/measurement"
    33  	"github.com/google/pprof/internal/plugin"
    34  	"github.com/google/pprof/profile"
    35  )
    36  
    37  // fetchProfiles fetches and symbolizes the profiles specified by s.
    38  // It will merge all the profiles it is able to retrieve, even if
    39  // there are some failures. It will return an error if it is unable to
    40  // fetch any profiles.
    41  func fetchProfiles(s *source, o *plugin.Options) (*profile.Profile, error) {
    42  	sources := make([]profileSource, 0, len(s.Sources))
    43  	for _, src := range s.Sources {
    44  		sources = append(sources, profileSource{
    45  			addr:   src,
    46  			source: s,
    47  		})
    48  	}
    49  
    50  	bases := make([]profileSource, 0, len(s.Base))
    51  	for _, src := range s.Base {
    52  		bases = append(bases, profileSource{
    53  			addr:   src,
    54  			source: s,
    55  		})
    56  	}
    57  
    58  	p, pbase, m, mbase, save, err := grabSourcesAndBases(sources, bases, o.Fetch, o.Obj, o.UI, o.HTTPTransport)
    59  	if err != nil {
    60  		return nil, err
    61  	}
    62  
    63  	if pbase != nil {
    64  		if s.DiffBase {
    65  			pbase.SetLabel("pprof::base", []string{"true"})
    66  		}
    67  		if s.Normalize {
    68  			err := p.Normalize(pbase)
    69  			if err != nil {
    70  				return nil, err
    71  			}
    72  		}
    73  		pbase.Scale(-1)
    74  		p, m, err = combineProfiles([]*profile.Profile{p, pbase}, []plugin.MappingSources{m, mbase})
    75  		if err != nil {
    76  			return nil, err
    77  		}
    78  	}
    79  
    80  	if s.AllFrames {
    81  		p.DropFrames = ""
    82  		p.KeepFrames = ""
    83  	}
    84  
    85  	// Symbolize the merged profile.
    86  	if err := o.Sym.Symbolize(s.Symbolize, m, p); err != nil {
    87  		return nil, err
    88  	}
    89  	p.RemoveUninteresting()
    90  	unsourceMappings(p)
    91  
    92  	if s.Comment != "" {
    93  		p.Comments = append(p.Comments, s.Comment)
    94  	}
    95  
    96  	// Save a copy of the merged profile if there is at least one remote source.
    97  	if save {
    98  		dir, err := setTmpDir(o.UI)
    99  		if err != nil {
   100  			return nil, err
   101  		}
   102  
   103  		prefix := "pprof."
   104  		if len(p.Mapping) > 0 && p.Mapping[0].File != "" {
   105  			prefix += filepath.Base(p.Mapping[0].File) + "."
   106  		}
   107  		for _, s := range p.SampleType {
   108  			prefix += s.Type + "."
   109  		}
   110  
   111  		tempFile, err := newTempFile(dir, prefix, ".pb.gz")
   112  		if err == nil {
   113  			if err = p.Write(tempFile); err == nil {
   114  				o.UI.PrintErr("Saved profile in ", tempFile.Name())
   115  			}
   116  		}
   117  		if err != nil {
   118  			o.UI.PrintErr("Could not save profile: ", err)
   119  		}
   120  	}
   121  
   122  	if err := p.CheckValid(); err != nil {
   123  		return nil, err
   124  	}
   125  
   126  	return p, nil
   127  }
   128  
   129  func grabSourcesAndBases(sources, bases []profileSource, fetch plugin.Fetcher, obj plugin.ObjTool, ui plugin.UI, tr http.RoundTripper) (*profile.Profile, *profile.Profile, plugin.MappingSources, plugin.MappingSources, bool, error) {
   130  	wg := sync.WaitGroup{}
   131  	wg.Add(2)
   132  	var psrc, pbase *profile.Profile
   133  	var msrc, mbase plugin.MappingSources
   134  	var savesrc, savebase bool
   135  	var errsrc, errbase error
   136  	var countsrc, countbase int
   137  	go func() {
   138  		defer wg.Done()
   139  		psrc, msrc, savesrc, countsrc, errsrc = chunkedGrab(sources, fetch, obj, ui, tr)
   140  	}()
   141  	go func() {
   142  		defer wg.Done()
   143  		pbase, mbase, savebase, countbase, errbase = chunkedGrab(bases, fetch, obj, ui, tr)
   144  	}()
   145  	wg.Wait()
   146  	save := savesrc || savebase
   147  
   148  	if errsrc != nil {
   149  		return nil, nil, nil, nil, false, fmt.Errorf("problem fetching source profiles: %v", errsrc)
   150  	}
   151  	if errbase != nil {
   152  		return nil, nil, nil, nil, false, fmt.Errorf("problem fetching base profiles: %v,", errbase)
   153  	}
   154  	if countsrc == 0 {
   155  		return nil, nil, nil, nil, false, fmt.Errorf("failed to fetch any source profiles")
   156  	}
   157  	if countbase == 0 && len(bases) > 0 {
   158  		return nil, nil, nil, nil, false, fmt.Errorf("failed to fetch any base profiles")
   159  	}
   160  	if want, got := len(sources), countsrc; want != got {
   161  		ui.PrintErr(fmt.Sprintf("Fetched %d source profiles out of %d", got, want))
   162  	}
   163  	if want, got := len(bases), countbase; want != got {
   164  		ui.PrintErr(fmt.Sprintf("Fetched %d base profiles out of %d", got, want))
   165  	}
   166  
   167  	return psrc, pbase, msrc, mbase, save, nil
   168  }
   169  
   170  // chunkedGrab fetches the profiles described in source and merges them into
   171  // a single profile. It fetches a chunk of profiles concurrently, with a maximum
   172  // chunk size to limit its memory usage.
   173  func chunkedGrab(sources []profileSource, fetch plugin.Fetcher, obj plugin.ObjTool, ui plugin.UI, tr http.RoundTripper) (*profile.Profile, plugin.MappingSources, bool, int, error) {
   174  	const chunkSize = 128
   175  
   176  	var p *profile.Profile
   177  	var msrc plugin.MappingSources
   178  	var save bool
   179  	var count int
   180  
   181  	for start := 0; start < len(sources); start += chunkSize {
   182  		end := min(start+chunkSize, len(sources))
   183  		chunkP, chunkMsrc, chunkSave, chunkCount, chunkErr := concurrentGrab(sources[start:end], fetch, obj, ui, tr)
   184  		switch {
   185  		case chunkErr != nil:
   186  			return nil, nil, false, 0, chunkErr
   187  		case chunkP == nil:
   188  			continue
   189  		case p == nil:
   190  			p, msrc, save, count = chunkP, chunkMsrc, chunkSave, chunkCount
   191  		default:
   192  			p, msrc, chunkErr = combineProfiles([]*profile.Profile{p, chunkP}, []plugin.MappingSources{msrc, chunkMsrc})
   193  			if chunkErr != nil {
   194  				return nil, nil, false, 0, chunkErr
   195  			}
   196  			if chunkSave {
   197  				save = true
   198  			}
   199  			count += chunkCount
   200  		}
   201  	}
   202  
   203  	return p, msrc, save, count, nil
   204  }
   205  
   206  // concurrentGrab fetches multiple profiles concurrently
   207  func concurrentGrab(sources []profileSource, fetch plugin.Fetcher, obj plugin.ObjTool, ui plugin.UI, tr http.RoundTripper) (*profile.Profile, plugin.MappingSources, bool, int, error) {
   208  	wg := sync.WaitGroup{}
   209  	wg.Add(len(sources))
   210  	for i := range sources {
   211  		go func(s *profileSource) {
   212  			defer wg.Done()
   213  			s.p, s.msrc, s.remote, s.err = grabProfile(s.source, s.addr, fetch, obj, ui, tr)
   214  		}(&sources[i])
   215  	}
   216  	wg.Wait()
   217  
   218  	var save bool
   219  	profiles := make([]*profile.Profile, 0, len(sources))
   220  	msrcs := make([]plugin.MappingSources, 0, len(sources))
   221  	for i := range sources {
   222  		s := &sources[i]
   223  		if err := s.err; err != nil {
   224  			ui.PrintErr(s.addr + ": " + err.Error())
   225  			continue
   226  		}
   227  		save = save || s.remote
   228  		profiles = append(profiles, s.p)
   229  		msrcs = append(msrcs, s.msrc)
   230  		*s = profileSource{}
   231  	}
   232  
   233  	if len(profiles) == 0 {
   234  		return nil, nil, false, 0, nil
   235  	}
   236  
   237  	p, msrc, err := combineProfiles(profiles, msrcs)
   238  	if err != nil {
   239  		return nil, nil, false, 0, err
   240  	}
   241  	return p, msrc, save, len(profiles), nil
   242  }
   243  
   244  func combineProfiles(profiles []*profile.Profile, msrcs []plugin.MappingSources) (*profile.Profile, plugin.MappingSources, error) {
   245  	// Merge profiles.
   246  	//
   247  	// The merge call below only treats exactly matching sample type lists as
   248  	// compatible and will fail otherwise. Make the profiles' sample types
   249  	// compatible for the merge, see CompatibilizeSampleTypes() doc for details.
   250  	if err := profile.CompatibilizeSampleTypes(profiles); err != nil {
   251  		return nil, nil, err
   252  	}
   253  	if err := measurement.ScaleProfiles(profiles); err != nil {
   254  		return nil, nil, err
   255  	}
   256  
   257  	// Avoid expensive work for the common case of a single profile/src.
   258  	if len(profiles) == 1 && len(msrcs) == 1 {
   259  		return profiles[0], msrcs[0], nil
   260  	}
   261  
   262  	p, err := profile.Merge(profiles)
   263  	if err != nil {
   264  		return nil, nil, err
   265  	}
   266  
   267  	// Combine mapping sources.
   268  	msrc := make(plugin.MappingSources)
   269  	for _, ms := range msrcs {
   270  		for m, s := range ms {
   271  			msrc[m] = append(msrc[m], s...)
   272  		}
   273  	}
   274  	return p, msrc, nil
   275  }
   276  
   277  type profileSource struct {
   278  	addr   string
   279  	source *source
   280  
   281  	p      *profile.Profile
   282  	msrc   plugin.MappingSources
   283  	remote bool
   284  	err    error
   285  }
   286  
   287  func homeEnv() string {
   288  	switch runtime.GOOS {
   289  	case "windows":
   290  		return "USERPROFILE"
   291  	case "plan9":
   292  		return "home"
   293  	default:
   294  		return "HOME"
   295  	}
   296  }
   297  
   298  // setTmpDir prepares the directory to use to save profiles retrieved
   299  // remotely. It is selected from PPROF_TMPDIR, defaults to $HOME/pprof, and, if
   300  // $HOME is not set, falls back to os.TempDir().
   301  func setTmpDir(ui plugin.UI) (string, error) {
   302  	var dirs []string
   303  	if profileDir := os.Getenv("PPROF_TMPDIR"); profileDir != "" {
   304  		dirs = append(dirs, profileDir)
   305  	}
   306  	if homeDir := os.Getenv(homeEnv()); homeDir != "" {
   307  		dirs = append(dirs, filepath.Join(homeDir, "pprof"))
   308  	}
   309  	dirs = append(dirs, os.TempDir())
   310  	for _, tmpDir := range dirs {
   311  		if err := os.MkdirAll(tmpDir, 0755); err != nil {
   312  			ui.PrintErr("Could not use temp dir ", tmpDir, ": ", err.Error())
   313  			continue
   314  		}
   315  		return tmpDir, nil
   316  	}
   317  	return "", fmt.Errorf("failed to identify temp dir")
   318  }
   319  
   320  const testSourceAddress = "pproftest.local"
   321  
   322  // grabProfile fetches a profile. Returns the profile, sources for the
   323  // profile mappings, a bool indicating if the profile was fetched
   324  // remotely, and an error.
   325  func grabProfile(s *source, source string, fetcher plugin.Fetcher, obj plugin.ObjTool, ui plugin.UI, tr http.RoundTripper) (p *profile.Profile, msrc plugin.MappingSources, remote bool, err error) {
   326  	var src string
   327  	duration, timeout := time.Duration(s.Seconds)*time.Second, time.Duration(s.Timeout)*time.Second
   328  	if fetcher != nil {
   329  		p, src, err = fetcher.Fetch(source, duration, timeout)
   330  		if err != nil {
   331  			return
   332  		}
   333  	}
   334  	if err != nil || p == nil {
   335  		// Fetch the profile over HTTP or from a file.
   336  		p, src, err = fetch(source, duration, timeout, ui, tr)
   337  		if err != nil {
   338  			return
   339  		}
   340  	}
   341  
   342  	if err = p.CheckValid(); err != nil {
   343  		return
   344  	}
   345  
   346  	// Update the binary locations from command line and paths.
   347  	locateBinaries(p, s, obj, ui)
   348  
   349  	// Collect the source URL for all mappings.
   350  	if src != "" {
   351  		msrc = collectMappingSources(p, src)
   352  		remote = true
   353  		if strings.HasPrefix(src, "http://"+testSourceAddress) {
   354  			// Treat test inputs as local to avoid saving
   355  			// testcase profiles during driver testing.
   356  			remote = false
   357  		}
   358  	}
   359  	return
   360  }
   361  
   362  // collectMappingSources saves the mapping sources of a profile.
   363  func collectMappingSources(p *profile.Profile, source string) plugin.MappingSources {
   364  	ms := plugin.MappingSources{}
   365  	for _, m := range p.Mapping {
   366  		src := struct {
   367  			Source string
   368  			Start  uint64
   369  		}{
   370  			source, m.Start,
   371  		}
   372  		key := m.BuildID
   373  		if key == "" {
   374  			key = m.File
   375  		}
   376  		if key == "" {
   377  			// If there is no build id or source file, use the source as the
   378  			// mapping file. This will enable remote symbolization for this
   379  			// mapping, in particular for Go profiles on the legacy format.
   380  			// The source is reset back to empty string by unsourceMapping
   381  			// which is called after symbolization is finished.
   382  			m.File = source
   383  			key = source
   384  		}
   385  		ms[key] = append(ms[key], src)
   386  	}
   387  	return ms
   388  }
   389  
   390  // unsourceMappings iterates over the mappings in a profile and replaces file
   391  // set to the remote source URL by collectMappingSources back to empty string.
   392  func unsourceMappings(p *profile.Profile) {
   393  	for _, m := range p.Mapping {
   394  		if m.BuildID == "" && filepath.VolumeName(m.File) == "" {
   395  			if u, err := url.Parse(m.File); err == nil && u.IsAbs() {
   396  				m.File = ""
   397  			}
   398  		}
   399  	}
   400  }
   401  
   402  // locateBinaries searches for binary files listed in the profile and, if found,
   403  // updates the profile accordingly.
   404  func locateBinaries(p *profile.Profile, s *source, obj plugin.ObjTool, ui plugin.UI) {
   405  	// Construct search path to examine
   406  	searchPath := os.Getenv("PPROF_BINARY_PATH")
   407  	if searchPath == "" {
   408  		// Use $HOME/pprof/binaries as default directory for local symbolization binaries
   409  		searchPath = filepath.Join(os.Getenv(homeEnv()), "pprof", "binaries")
   410  	}
   411  mapping:
   412  	for _, m := range p.Mapping {
   413  		var noVolumeFile string
   414  		var baseName string
   415  		var dirName string
   416  		if m.File != "" {
   417  			noVolumeFile = strings.TrimPrefix(m.File, filepath.VolumeName(m.File))
   418  			baseName = filepath.Base(m.File)
   419  			dirName = filepath.Dir(noVolumeFile)
   420  		}
   421  
   422  		for _, path := range filepath.SplitList(searchPath) {
   423  			var fileNames []string
   424  			if m.BuildID != "" {
   425  				fileNames = []string{filepath.Join(path, m.BuildID, baseName)}
   426  				if matches, err := filepath.Glob(filepath.Join(path, m.BuildID, "*")); err == nil {
   427  					fileNames = append(fileNames, matches...)
   428  				}
   429  				fileNames = append(fileNames, filepath.Join(path, noVolumeFile, m.BuildID)) // perf path format
   430  				// Llvm buildid protocol: the first two characters of the build id
   431  				// are used as directory, and the remaining part is in the filename.
   432  				// e.g. `/ab/cdef0123456.debug`
   433  				fileNames = append(fileNames, filepath.Join(path, m.BuildID[:2], m.BuildID[2:]+".debug"))
   434  			}
   435  			if m.File != "" {
   436  				// Try both the basename and the full path, to support the same directory
   437  				// structure as the perf symfs option.
   438  				fileNames = append(fileNames, filepath.Join(path, baseName))
   439  				fileNames = append(fileNames, filepath.Join(path, noVolumeFile))
   440  				// Other locations: use the same search paths as GDB, according to
   441  				// https://sourceware.org/gdb/onlinedocs/gdb/Separate-Debug-Files.html
   442  				fileNames = append(fileNames, filepath.Join(path, noVolumeFile+".debug"))
   443  				fileNames = append(fileNames, filepath.Join(path, dirName, ".debug", baseName+".debug"))
   444  				fileNames = append(fileNames, filepath.Join(path, "usr", "lib", "debug", dirName, baseName+".debug"))
   445  			}
   446  			for _, name := range fileNames {
   447  				if f, err := obj.Open(name, m.Start, m.Limit, m.Offset, m.KernelRelocationSymbol); err == nil {
   448  					defer f.Close()
   449  					fileBuildID := f.BuildID()
   450  					if m.BuildID != "" && m.BuildID != fileBuildID {
   451  						ui.PrintErr("Ignoring local file " + name + ": build-id mismatch (" + m.BuildID + " != " + fileBuildID + ")")
   452  					} else {
   453  						// Explicitly do not update KernelRelocationSymbol --
   454  						// the new local file name is most likely missing it.
   455  						m.File = name
   456  						continue mapping
   457  					}
   458  				}
   459  			}
   460  		}
   461  	}
   462  	if len(p.Mapping) == 0 {
   463  		// If there are no mappings, add a fake mapping to attempt symbolization.
   464  		// This is useful for some profiles generated by the golang runtime, which
   465  		// do not include any mappings. Symbolization with a fake mapping will only
   466  		// be successful against a non-PIE binary.
   467  		m := &profile.Mapping{ID: 1}
   468  		p.Mapping = []*profile.Mapping{m}
   469  		for _, l := range p.Location {
   470  			l.Mapping = m
   471  		}
   472  	}
   473  	// If configured, apply executable filename override and (maybe, see below)
   474  	// build ID override from source. Assume the executable is the first mapping.
   475  	if execName, buildID := s.ExecName, s.BuildID; execName != "" || buildID != "" {
   476  		m := p.Mapping[0]
   477  		if execName != "" {
   478  			// Explicitly do not update KernelRelocationSymbol --
   479  			// the source override is most likely missing it.
   480  			m.File = execName
   481  		}
   482  		// Only apply the build ID override if the build ID in the main mapping is
   483  		// missing. Overwriting the build ID in case it's present is very likely a
   484  		// wrong thing to do so we refuse to do that.
   485  		if buildID != "" && m.BuildID == "" {
   486  			m.BuildID = buildID
   487  		}
   488  	}
   489  }
   490  
   491  // fetch fetches a profile from source, within the timeout specified,
   492  // producing messages through the ui. It returns the profile and the
   493  // url of the actual source of the profile for remote profiles.
   494  func fetch(source string, duration, timeout time.Duration, ui plugin.UI, tr http.RoundTripper) (p *profile.Profile, src string, err error) {
   495  	var f io.ReadCloser
   496  
   497  	// First determine whether the source is a file, if not, it will be treated as a URL.
   498  	if _, err = os.Stat(source); err == nil {
   499  		if isPerfFile(source) {
   500  			f, err = convertPerfData(source, ui)
   501  		} else {
   502  			f, err = os.Open(source)
   503  		}
   504  	} else {
   505  		sourceURL, timeout := adjustURL(source, duration, timeout)
   506  		if sourceURL != "" {
   507  			ui.Print("Fetching profile over HTTP from " + sourceURL)
   508  			if duration > 0 {
   509  				ui.Print(fmt.Sprintf("Please wait... (%v)", duration))
   510  			}
   511  			f, err = fetchURL(sourceURL, timeout, tr)
   512  			src = sourceURL
   513  		}
   514  	}
   515  	if err == nil {
   516  		defer f.Close()
   517  		p, err = profile.Parse(f)
   518  	}
   519  	return
   520  }
   521  
   522  // fetchURL fetches a profile from a URL using HTTP.
   523  func fetchURL(source string, timeout time.Duration, tr http.RoundTripper) (io.ReadCloser, error) {
   524  	client := &http.Client{
   525  		Transport: tr,
   526  		Timeout:   timeout + 5*time.Second,
   527  	}
   528  	resp, err := client.Get(source)
   529  	if err != nil {
   530  		return nil, fmt.Errorf("http fetch: %v", err)
   531  	}
   532  	if resp.StatusCode != http.StatusOK {
   533  		defer resp.Body.Close()
   534  		return nil, statusCodeError(resp)
   535  	}
   536  
   537  	return resp.Body, nil
   538  }
   539  
   540  func statusCodeError(resp *http.Response) error {
   541  	if resp.Header.Get("X-Go-Pprof") != "" && strings.Contains(resp.Header.Get("Content-Type"), "text/plain") {
   542  		// error is from pprof endpoint
   543  		if body, err := io.ReadAll(resp.Body); err == nil {
   544  			return fmt.Errorf("server response: %s - %s", resp.Status, body)
   545  		}
   546  	}
   547  	return fmt.Errorf("server response: %s", resp.Status)
   548  }
   549  
   550  // isPerfFile checks if a file is in perf.data format. It also returns false
   551  // if it encounters an error during the check.
   552  func isPerfFile(path string) bool {
   553  	sourceFile, openErr := os.Open(path)
   554  	if openErr != nil {
   555  		return false
   556  	}
   557  	defer sourceFile.Close()
   558  
   559  	// If the file is the output of a perf record command, it should begin
   560  	// with the string PERFILE2.
   561  	perfHeader := []byte("PERFILE2")
   562  	actualHeader := make([]byte, len(perfHeader))
   563  	if _, readErr := sourceFile.Read(actualHeader); readErr != nil {
   564  		return false
   565  	}
   566  	return bytes.Equal(actualHeader, perfHeader)
   567  }
   568  
   569  // convertPerfData converts the file at path which should be in perf.data format
   570  // using the perf_to_profile tool and returns the file containing the
   571  // profile.proto formatted data.
   572  func convertPerfData(perfPath string, ui plugin.UI) (*os.File, error) {
   573  	ui.Print(fmt.Sprintf(
   574  		"Converting %s to a profile.proto... (May take a few minutes)",
   575  		perfPath))
   576  	profile, err := newTempFile(os.TempDir(), "pprof_", ".pb.gz")
   577  	if err != nil {
   578  		return nil, err
   579  	}
   580  	deferDeleteTempFile(profile.Name())
   581  	cmd := exec.Command("perf_to_profile", "-i", perfPath, "-o", profile.Name(), "-f")
   582  	cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
   583  	if err := cmd.Run(); err != nil {
   584  		profile.Close()
   585  		return nil, fmt.Errorf("failed to convert perf.data file. Try github.com/google/perf_data_converter: %v", err)
   586  	}
   587  	return profile, nil
   588  }
   589  
   590  // adjustURL validates if a profile source is a URL and returns an
   591  // cleaned up URL and the timeout to use for retrieval over HTTP.
   592  // If the source cannot be recognized as a URL it returns an empty string.
   593  func adjustURL(source string, duration, timeout time.Duration) (string, time.Duration) {
   594  	u, err := url.Parse(source)
   595  	if err != nil || (u.Host == "" && u.Scheme != "" && u.Scheme != "file") {
   596  		// Try adding http:// to catch sources of the form hostname:port/path.
   597  		// url.Parse treats "hostname" as the scheme.
   598  		u, err = url.Parse("http://" + source)
   599  	}
   600  	if err != nil || u.Host == "" {
   601  		return "", 0
   602  	}
   603  
   604  	// Apply duration/timeout overrides to URL.
   605  	values := u.Query()
   606  	if duration > 0 {
   607  		values.Set("seconds", fmt.Sprint(int(duration.Seconds())))
   608  	} else {
   609  		if urlSeconds := values.Get("seconds"); urlSeconds != "" {
   610  			if us, err := strconv.ParseInt(urlSeconds, 10, 32); err == nil {
   611  				duration = time.Duration(us) * time.Second
   612  			}
   613  		}
   614  	}
   615  	if timeout <= 0 {
   616  		if duration > 0 {
   617  			timeout = duration + duration/2
   618  		} else {
   619  			timeout = 60 * time.Second
   620  		}
   621  	}
   622  	u.RawQuery = values.Encode()
   623  	return u.String(), timeout
   624  }
   625  

View as plain text