[
  {
    "path": ".dockerignore",
    "content": "/dist\n"
  },
  {
    "path": ".github/renovate.json",
    "content": "{\n  \"extends\": [\"config:base\"],\n  \"labels\": [\"renovate\"],\n  \"enabledManagers\": [\"dockerfile\", \"regex\", \"github-actions\"],\n  \"regexManagers\": [\n    {\n      \"fileMatch\": [\"(^|/)Makefile$\"],\n      \"matchStrings\": [\n        \"#\\\\s*renovate:\\\\s*datasource=(?<datasource>.*?)\\\\s+depName=(?<depName>.*?)(\\\\s+versioning=(?<versioning>.*?))?(\\\\s+registry=(?<registryUrl>.*?))?\\\\s.*?_VERSION\\\\s+[^=]?=\\\\s+(?<currentValue>.*)\\\\s\"\n      ],\n      \"versioningTemplate\": \"{{#if versioning}}{{{versioning}}}{{else}}semver{{/if}}\"\n    }\n  ]\n}\n"
  },
  {
    "path": ".github/workflows/ci.yaml",
    "content": "name: CI\n\non:\n  push:\n    branches: [master]\n    paths-ignore: ['**.md']\n  pull_request:\n    types: [opened, synchronize]\n    paths-ignore: ['**.md']\n\njobs:\n  run:\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@v4\n    - uses: actions/setup-go@v5\n      with:\n        go-version-file: go.mod\n    - name: Ensure go.mod is already tidied\n      run: go mod tidy && git diff -s --exit-code go.sum\n    - run: make lint\n    - run: make test\n    - run: make build\n"
  },
  {
    "path": ".github/workflows/release.yaml",
    "content": "name: Release\n\non:\n  push:\n    tags: [\"v*\"]\n\njobs:\n  run:\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@v4\n    - uses: actions/setup-go@v5\n      with:\n        go-version-file: go.mod\n    - run: make release\n      env:\n        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".gitignore",
    "content": "/dist\n/hack/tools/bin\n"
  },
  {
    "path": ".goreleaser.yaml",
    "content": "version: 2\nbefore:\n  hooks:\n  - go mod tidy\nproject_name: opener\nbuilds:\n- env:\n  - CGO_ENABLED=0\n  goos:\n  - linux\n  - darwin\n  goarch:\n  - amd64\n  - arm\n  - arm64\narchives:\n- name_template: \"{{ .ProjectName }}-{{ .Os }}-{{ .Arch }}\"\n  formats:\n  - zip\n  files:\n  - LICENSE\n  - README.md\n  wrap_in_directory: false\nchecksum:\n  name_template: 'checksums.txt'\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM --platform=${BUILDPLATFORM} golang:1.24 AS base\nWORKDIR /src\nENV CGO_ENABLED=0\nCOPY go.* .\nRUN --mount=type=cache,target=/go/pkg/mod \\\n    go mod download\n\nFROM base AS build\nARG TARGETOS\nARG TARGETARCH\nRUN --mount=target=. \\\n    --mount=type=cache,target=/go/pkg/mod \\\n    --mount=type=cache,target=/root/.cache/go-build \\\n    GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -o /out/opener .\n\nFROM base AS test\nRUN --mount=target=. \\\n    --mount=type=cache,target=/go/pkg/mod \\\n    --mount=type=cache,target=/root/.cache/go-build \\\n    go test -v ./...\n\nFROM golangci/golangci-lint:v1.64.8 AS lint-base\nFROM base AS lint\nRUN --mount=target=. \\\n    --mount=from=lint-base,src=/usr/bin/golangci-lint,target=/usr/bin/golangci-lint \\\n    --mount=type=cache,target=/go/pkg/mod \\\n    --mount=type=cache,target=/root/.cache/go-build \\\n    --mount=type=cache,target=/root/.cache/golangci-lint \\\n    go vet ./... && \\\n    go fmt ./... && \\\n    golangci-lint run\n\nFROM scratch AS bin-unix\nCOPY --from=build /out/opener /\n\nFROM bin-unix AS bin-linux\nFROM bin-unix AS bin-darwin\n\nFROM bin-${TARGETOS} as bin\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2021 Kazuki Suda <kazuki.suda@gmail.com>\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "Makefile",
    "content": "NAME := opener\nDIST_DIR := dist\nGO ?= go\nVERSION ?= $(shell git describe --tags --always --dirty)\n\nPLATFORM ?= local\nDOCKER ?= DOCKER_BUILDKIT=1 docker\n\n.PHONY: build\nbuild:\n\t$(DOCKER) build --target bin --output $(DIST_DIR) --platform $(PLATFORM) .\n\nTOOLS_BIN_DIR := $(CURDIR)/hack/tools/bin\n$(shell mkdir -p $(TOOLS_BIN_DIR))\n\n# renovate: datasource=github-releases depName=goreleaser/goreleaser\nGORELEASER_VERSION ?= v2.8.1\nGORELEASER := $(TOOLS_BIN_DIR)/goreleaser\n\n$(GORELEASER):\n\tGOBIN=$(TOOLS_BIN_DIR) $(GO) install github.com/goreleaser/goreleaser/v2@$(GORELEASER_VERSION)\n\n.PHONY: build-cross\nbuild-cross: $(GORELEASER)\n\t$(GORELEASER) build --snapshot --clean\n\n.PHONY: test\ntest:\n\t$(DOCKER) build --target test .\n\n.PHONY: lint\nlint:\n\t$(DOCKER) build --target lint .\n\n.PHONY: dist\ndist: $(GORELEASER)\n\t$(GORELEASER) release --clean --skip=publish --snapshot\n\n.PHONY: release\nrelease: $(GORELEASER)\n\t$(GORELEASER) release --clean --skip=validate\n\n.PHONY: clean\nclean: clean-tools clean-dist\n\n.PHONY: clean-tools\nclean-tools:\n\trm -rf $(TOOLS_BIN_DIR)\n\n.PHONY: clean-dist\nclean-dist:\n\trm -rf $(DIST_DIR)\n"
  },
  {
    "path": "README.md",
    "content": "# opener\n\n![Logo](./sennuki.png)\n\nOpen URL in your local web browser from the SSH-connected remote environment.\n\n## How does opener work?\n\nopener is a daemon process that runs locally. When you send a URL to the process, it will execute a command tailored to your local environment (`open` on macOS, `xdg-open` on Linux) with the URL as an argument. As a result, the URL will be opened in your favorite web browser.\n\nYou remotely forward the socket file of the opener daemon, `~/.opener.sock`, when you log in to the remote environment via SSH. In a remote environment, you use fake `open` command or` xdg-open` command to send the URL to `~/.opener.sock` being forwarded from your local environment. The result is as if URL was sent to the local opener daemon, which opens the URL in your local web browser.\n\n```\n┌────────────────────┐                 ┌────────────────────┐\n│                    │                 │                    │\n│ ┌────────────────┐ │                 │ ┌────────────────┐ │\n│ │   Web Browser  │ │                 │ │  open command  │ │\n│ └─▲──────────────┘ │                 │ │     (fake)     │ │\n│   │ Open URL       │                 │ └─┬──────────────┘ │\n│ ┌─┴──────────────┐ │                 │   │                │\n│ │  opener daemon │ │                 │   │ Send URL       │\n│ └─┬──────────────┘ │                 │   │                │\n│   │                │                 │   │                │\n│ ┌─┴──────────────┐ │ SSH connection  │ ┌─▼──────────────┐ │\n│ │ ~/.opener.sock │ ├─────────────────► │ ~/.opener.sock │ │\n│ └────────────────┘ │ Remote forward  │ └────────────────┘ │\n│                    │                 │                    │\n│      localhost     │                 │    remote server   │\n└────────────────────┘                 └────────────────────┘\n```\n\n## Setup\n\n### Local environment\n\nYou can install opener with Homebrew. Since opener is a daemon, it is managed by Homebrew-services.\n\n```\n$ brew install superbrothers/opener/opener\n$ brew services start opener\n```\n\nSet ssh config to forward `~/.opener.sock` to the remote environment.\n\n```\nHost host.example.org\n  RemoteForward /home/me/.opener.sock /Users/me/.opener.sock\n```\n\n### Remote environment\n\nInstall a fake `open` or` xdg-open` command. Please choose your preference either way.\n\n```sh\n$ mkdir ~/bin\n# open command\n$ curl -L -o ~/bin/open https://raw.githubusercontent.com/superbrothers/opener/master/bin/open\n$ chmod 755 ~/bin/open\n# xdg-open command\n$ curl -L -o ~/bin/xdg-open https://raw.githubusercontent.com/superbrothers/opener/master/bin/xdg-open\n$ chmod 755 ~/bin/xdg-open\n# Add ~/bin to $PATH and enable it\n$ echo 'export PATH=\"$HOME/bin:$PATH\"' >>~/.bashrc\n$ source ~/.bashrc\n```\n\nFake commands use `nc` command, so install it if you don't have it.\n\n```sh\n# Ubuntu 20.04\n$ sudo apt install netcat\n```\n\nAdd the following settings to sshd. This is an option to delete the socket file when you lose the connection to the remote environment.\n\n```sh\n# Add a configuration file\n$ echo \"StreamLocalBindUnlink yes\" | sudo tee /etc/ssh/sshd_config.d/opener.conf\n# Restart ssh service\n$ sudo systemctl restart ssh\n```\n\n## How to use it\n\nIf set up correctly, the following command in a remote environment will send the URL through opener and open the URL in your local web browser.\n\n```\n$ open https://www.google.com/\n```\n\n## Configuration\n\nYou can configure opener with a config file. By default, it should be located at `~/.config/opener/config.yaml`. You can also specify a config file with `--config` option.\n\n```yaml\n# The network to use opener daemon.\n# Allowed networks are: unix or tcp. (defaults to unix)\nnetwork: unix\n\n# The address to listen on. (defaults to ~/.opener.sock)\naddress: ~/.opener.sock\n```\n\n### Example: Open a URL from inside a container\n\nIf you want to open a URL from inside a container, you can use `tcp` network instead of `unix`.\n\n```\n┌─────────────────────────────────────────────────────────┐\n│                                                         │\n│ ┌────────────────┐                                      │\n│ │   Web Browser  │                                      │\n│ └─▲──────────────┘                 ┌──────────────────┐ │\n│   │                                │     container    │ │\n│   │ Open URL                       │                  │ │\n│   │                                │ ┌──────────────┐ │ │\n│ ┌─┴──────────────┐    Send a URL   │ │ open command │ │ │\n│ │  opener daemon │◄────────────────┼─┤    (fake)    │ │ │\n│ └────────────────┘   (TCP request) │ └──────────────┘ │ │\n│   127.0.0.1:9999                   │                  │ │\n│                                    └──────────────────┘ │\n│                       localhost                         │\n└─────────────────────────────────────────────────────────┘\n```\n\nCreate the following config at `~/.config/opener/config.yaml`:\n\n```yaml\nnetwork: tcp\naddress: 127.0.0.1:9999\n```\n\nRestart the opener daemon:\n\n```\n$ brew services restart opener\n```\n\nSend a URL to the opener daemon from inside a container:\n\n```\n$ docker run --rm -it busybox /bin/sh\n# echo https://www.google.com/ | nc host.docker.internal 9999\n```\n\nThe following script is useful as a fake `open` command.\n\n```sh\n#!/bin/sh\necho \"$@\" | nc host.docker.internal 9999\n```\n\n## License\n\nMIT License\n"
  },
  {
    "path": "bin/open",
    "content": "#!/bin/bash\n\nset -eu\n\n# get data either form stdin or from file\nif (( $# == 0 )) ; then\n  # if no argument, read from standard input from pipe\n  buf=$(cat \"$@\")\nelse\n  # otherwise read from all arguments\n  buf=$@\nfi\n\necho \"$buf\" | nc -U \"$HOME/.opener.sock\"\n"
  },
  {
    "path": "bin/xdg-open",
    "content": "#!/bin/bash\n\nset -eu\n\n# get data either form stdin or from file\nif (( $# == 0 )) ; then\n  # if no argument, read from standard input from pipe\n  buf=$(cat \"$@\")\nelse\n  # otherwise read from all arguments\n  buf=$@\nfi\n\necho \"$buf\" | nc -U \"$HOME/.opener.sock\"\n"
  },
  {
    "path": "config.go",
    "content": "package main\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"sigs.k8s.io/yaml\"\n)\n\nfunc LoadOpenerOptionsFromConfig(configPath string, o *OpenerOptions) error {\n\tif configPath == \"\" {\n\t\tdir, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tconfigPath = filepath.Join(dir, \".config\", \"opener\", \"config.yaml\")\n\t\tif _, err := os.Stat(configPath); err != nil {\n\t\t\t// The config file does not exist in the default path.\n\t\t\treturn nil\n\t\t}\n\t} else {\n\t\tif _, err := os.Stat(configPath); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tb, err := os.ReadFile(configPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif err := yaml.Unmarshal(b, o); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "config_test.go",
    "content": "package main\n\nimport (\n\t\"path/filepath\"\n\t\"reflect\"\n\t\"testing\"\n)\n\nfunc TestLoadOptionsFromConfig(t *testing.T) {\n\ttt := []struct {\n\t\ttest        string\n\t\tconfigPath  string\n\t\texpected    *OpenerOptions\n\t\texpectedErr string\n\t}{\n\t\t{\n\t\t\t\"unix\",\n\t\t\tfilepath.Join(\"testdata\", \"config\", \"unix.yaml\"),\n\t\t\t&OpenerOptions{\n\t\t\t\tNetwork: \"unix\",\n\t\t\t\tAddress: \"~/.opener.sock\",\n\t\t\t},\n\t\t\t\"\",\n\t\t},\n\t\t{\n\t\t\t\"tcp\",\n\t\t\tfilepath.Join(\"testdata\", \"config\", \"tcp.yaml\"),\n\t\t\t&OpenerOptions{\n\t\t\t\tNetwork: \"tcp\",\n\t\t\t\tAddress: \"127.0.0.1:9000\",\n\t\t\t},\n\t\t\t\"\",\n\t\t},\n\t\t{\n\t\t\t\"empty\",\n\t\t\tfilepath.Join(\"testdata\", \"config\", \"empty.yaml\"),\n\t\t\t&OpenerOptions{},\n\t\t\t\"\",\n\t\t},\n\t\t{\n\t\t\t\"no such file\",\n\t\t\tfilepath.Join(\"testdata\", \"config\", \"no-such-file.yaml\"),\n\t\t\t&OpenerOptions{},\n\t\t\t\"stat testdata/config/no-such-file.yaml: no such file or directory\",\n\t\t},\n\t}\n\n\tfor _, tc := range tt {\n\t\tt.Run(tc.test, func(t *testing.T) {\n\t\t\to := &OpenerOptions{}\n\t\t\terr := LoadOpenerOptionsFromConfig(tc.configPath, o)\n\t\t\tif err == nil {\n\t\t\t\tif tc.expectedErr != \"\" {\n\t\t\t\t\tt.Errorf(\"expected err nil, but %q\", err)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif tc.expectedErr != err.Error() {\n\t\t\t\t\tt.Errorf(\"expected err %q, but %q\", tc.expectedErr, err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif !reflect.DeepEqual(tc.expected, o) {\n\t\t\t\tt.Errorf(\"expected %#v, but %#v\", tc.expected, o)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/superbrothers/opener\n\ngo 1.24.1\n\nrequire (\n\tgithub.com/mitchellh/go-homedir v1.1.0\n\tgithub.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c\n\tgithub.com/spf13/cobra v1.9.1\n\tsigs.k8s.io/yaml v1.4.0\n)\n\nrequire (\n\tgithub.com/inconshreveable/mousetrap v1.1.0 // indirect\n\tgithub.com/spf13/pflag v1.0.6 // indirect\n\tgolang.org/x/sys v0.31.0 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=\ngithub.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=\ngithub.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=\ngithub.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=\ngithub.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=\ngithub.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=\ngithub.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=\ngithub.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=\ngithub.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=\ngithub.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=\ngithub.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=\ngithub.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngolang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=\ngolang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\nsigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=\nsigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=\n"
  },
  {
    "path": "main.go",
    "content": "package main\n\nimport (\n\t\"log\"\n\t\"os\"\n)\n\nfunc main() {\n\tcmd := NewOpenerCmd(os.Stderr)\n\tif err := cmd.Execute(); err != nil {\n\t\tlog.Fatal(err)\n\t}\n}\n"
  },
  {
    "path": "opener.go",
    "content": "package main\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"os\"\n\t\"os/signal\"\n\t\"strings\"\n\t\"sync\"\n\t\"syscall\"\n\n\t\"github.com/mitchellh/go-homedir\"\n\t\"github.com/pkg/browser\"\n\t\"github.com/spf13/cobra\"\n)\n\nvar version string\nvar commit string\nvar date string\n\ntype OpenerOptions struct {\n\tNetwork string `yaml:\"network\"`\n\tAddress string `yaml:\"address\"`\n\n\tErrOut io.Writer\n}\n\nfunc NewOpenerCmd(errOut io.Writer) *cobra.Command {\n\tvar configPath string\n\n\to := &OpenerOptions{\n\t\tNetwork: \"unix\",\n\t\tAddress: \"~/.opener.sock\",\n\t\tErrOut:  errOut,\n\t}\n\n\tcmd := &cobra.Command{\n\t\tUse: \"opener\",\n\t\tRunE: func(_ *cobra.Command, args []string) error {\n\t\t\tif err := LoadOpenerOptionsFromConfig(configPath, o); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif err := o.Validate(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\treturn o.Run()\n\t\t},\n\t}\n\n\tcmd.Flags().StringVar(&configPath, \"config\", configPath, \"Path to the opener config file (defaults to ~/.config/opener/config.yaml)\")\n\n\treturn cmd\n}\n\nfunc (o *OpenerOptions) Validate() error {\n\tswitch o.Network {\n\tcase \"unix\":\n\t\taddress, err := homedir.Expand(o.Address)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\to.Address = address\n\n\t\tsyscall.Umask(0077)\n\n\t\tif err := os.RemoveAll(o.Address); err != nil {\n\t\t\treturn err\n\t\t}\n\tcase \"tcp\":\n\tdefault:\n\t\treturn errors.New(\"allowed network are: unix,tcp\")\n\t}\n\n\treturn nil\n}\n\nfunc (o *OpenerOptions) Run() error {\n\tfmt.Fprintf(o.ErrOut, \"version: %s, commit: %s, date: %s\\n\", version, commit, date)\n\tfmt.Fprintf(o.ErrOut, \"starting a server at %s\\n\", o.Address)\n\n\tln, err := net.Listen(o.Network, o.Address)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdefer ln.Close()\n\n\tgo func() {\n\t\tfor {\n\t\t\tconn, err := ln.Accept()\n\t\t\tif err != nil {\n\t\t\t\tfmt.Fprintln(o.ErrOut, err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tgo handleConnection(conn, o.ErrOut)\n\t\t}\n\t}()\n\n\tc := make(chan os.Signal, 1)\n\tsignal.Notify(c, os.Interrupt, syscall.SIGTERM)\n\tsig := <-c\n\tfmt.Fprintf(o.ErrOut, \"got signal %s\\n\", sig)\n\n\treturn nil\n}\n\nvar browserMu sync.Mutex\n\nvar openURL = func(line string) (string, error) {\n\t// We try out best avoiding race-condition on swapping browser.{Stdout,Stderr}.\n\t// This works in a case when there are two or more consumers exist for this package.\n\t//\n\t// Fingers-crossed when github.com/pkg/browser is used concurrently outside of this package...\n\tbrowserMu.Lock()\n\n\tstdout, stderr := browser.Stdout, browser.Stderr\n\n\tdefer func() {\n\t\tbrowser.Stdout = stdout\n\t\tbrowser.Stderr = stderr\n\n\t\tbrowserMu.Unlock()\n\t}()\n\n\tvar buf bytes.Buffer\n\n\tbrowser.Stdout = &buf\n\tbrowser.Stderr = &buf\n\n\terr := browser.OpenURL(line)\n\n\treturn buf.String(), err\n}\n\nfunc handleConnection(conn net.Conn, errOut io.Writer) {\n\tdefer conn.Close()\n\n\tline, err := bufio.NewReader(conn).ReadString('\\n')\n\tline = strings.TrimRight(line, \"\\n\")\n\tfmt.Fprintf(errOut, \"received %q\\n\", line)\n\tif err != nil {\n\t\tif err != io.EOF {\n\t\t\tfmt.Fprintln(errOut, err)\n\t\t\treturn\n\t\t}\n\t}\n\n\tlogs, err := openURL(line)\n\n\tif logs != \"\" {\n\t\tfmt.Fprint(errOut, logs)\n\t}\n\n\tif err != nil {\n\t\tfmt.Fprintf(errOut, \"failed to open %q: %v\\n\", line, err)\n\n\t\t// Send back the logs from `open` to the client over e.g. the unix domain socket, so that\n\t\t// `open` on the client machine would work more like that on the server.\n\t\t//\n\t\t// Note that this works only when the client selected the protocol of SOCK_STREAM rather than e.g. SOCK_DGRAM.\n\t\t// `socat`, for example, negotiates the protocol to prefer SOCK_STREAM so you won't usually care.\n\t\tif _, err := conn.Write([]byte(logs)); err != nil {\n\t\t\tfmt.Fprintf(errOut, \"failed to send error to client: %v\\n\", err)\n\t\t}\n\t\treturn\n\t}\n}\n"
  },
  {
    "path": "opener_test.go",
    "content": "package main\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"math/rand\"\n\t\"net\"\n\t\"path/filepath\"\n\t\"testing\"\n)\n\nfunc TestOpenerOptionsValidate(t *testing.T) {\n\ttt := []struct {\n\t\ttest        string\n\t\to           *OpenerOptions\n\t\texpectedErr string\n\t}{\n\t\t{\n\t\t\t\"unix domain socket can be used\",\n\t\t\t&OpenerOptions{\n\t\t\t\tNetwork: \"unix\",\n\t\t\t\tAddress: filepath.Join(\"/\", \"tmp\", fmt.Sprintf(\"%03d\", rand.Intn(1000)), \"opener.sock\"),\n\t\t\t},\n\t\t\t\"\",\n\t\t},\n\t\t{\n\t\t\t\"tcp can be used\",\n\t\t\t&OpenerOptions{\n\t\t\t\tNetwork: \"tcp\",\n\t\t\t\tAddress: \"127.0.0.1:8888\",\n\t\t\t},\n\t\t\t\"\",\n\t\t},\n\t\t{\n\t\t\t\"udp cannot be used\",\n\t\t\t&OpenerOptions{\n\t\t\t\tNetwork: \"udp\",\n\t\t\t\tAddress: \"127.0.0.1:8888\",\n\t\t\t},\n\t\t\t\"allowed network are: unix,tcp\",\n\t\t},\n\t}\n\n\tfor _, tc := range tt {\n\t\tt.Run(tc.test, func(t *testing.T) {\n\t\t\terr := tc.o.Validate()\n\t\t\tif err == nil {\n\t\t\t\tif tc.expectedErr != \"\" {\n\t\t\t\t\tt.Errorf(\"expect err nil, but actual %q\", err)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif tc.expectedErr != err.Error() {\n\t\t\t\t\tt.Errorf(\"expect err %q, but actual %q\", tc.expectedErr, err)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestHandleConnection(t *testing.T) {\n\ttt := []struct {\n\t\ttest        string\n\t\topenURLFunc func(string) (string, error)\n\t\tdata        string\n\t\terr         error\n\t}{\n\t\t{\n\t\t\t\"Say nothing when successful\",\n\t\t\tfunc(line string) (string, error) {\n\t\t\t\treturn \"pong\\n\", nil\n\t\t\t},\n\t\t\t\"\",\n\t\t\tio.EOF,\n\t\t},\n\t\t{\n\t\t\t\"Sending back the logs when failure\",\n\t\t\tfunc(line string) (string, error) {\n\t\t\t\treturn \"pong\\n\", errors.New(\"exit status 1\")\n\t\t\t},\n\t\t\t\"pong\\n\",\n\t\t\tnil,\n\t\t},\n\t}\n\n\tln, _ := net.Listen(\"tcp\", \"127.0.0.1:0\")\n\tdefer ln.Close()\n\n\tfor _, tc := range tt {\n\t\tt.Run(tc.test, func(t *testing.T) {\n\t\t\topenURL = tc.openURLFunc\n\n\t\t\tgo func() {\n\t\t\t\tconn, _ := ln.Accept()\n\t\t\t\tgo handleConnection(conn, io.Discard)\n\t\t\t}()\n\n\t\t\tclient, err := net.Dial(\"tcp\", ln.Addr().String())\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tdefer client.Close()\n\n\t\t\tif _, err := client.Write([]byte(\"ping\\n\")); err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tbuf := make([]byte, 1024)\n\t\t\tn, err := client.Read(buf)\n\t\t\tdata := string(buf[:n])\n\t\t\tif tc.data != data {\n\t\t\t\tt.Errorf(\"expect %q, but actual %q\", tc.data, data)\n\t\t\t}\n\n\t\t\tif tc.err != err {\n\t\t\t\tt.Errorf(\"expect %v, but actual %v\", tc.err, err)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "testdata/config/empty.yaml",
    "content": ""
  },
  {
    "path": "testdata/config/tcp.yaml",
    "content": "network: tcp\naddress: 127.0.0.1:9000\n"
  },
  {
    "path": "testdata/config/unix.yaml",
    "content": "network: unix\naddress: ~/.opener.sock\n"
  }
]