We're hiring
Products

Channels

Beams

Chatkit

DocsTutorialsSupportCareersPusher Blog
Sign InSign Up
Products

Channels

Build scalable, realtime features into your apps

Features Pricing

Beams

Send push notifications programmatically at scale

Pricing

Chatkit

Build chat into your app in hours, not days

Pricing
Developers

Docs

Read the docs to learn how to use our products

Channels Beams Chatkit

Tutorials

Explore our tutorials to build apps with Pusher products

Support

Reach out to our support team for help and advice

Status

Check on the status of any of our products

Products

Channels

Build scalable, realtime features into your apps

Features Pricing

Beams

Send push notifications programmatically at scale

Pricing

Chatkit

Build chat into your app in hours, not days

Pricing
Developers

Docs

Read the docs to learn how to use our products

Channels Beams Chatkit

Tutorials

Explore our tutorials to build apps with Pusher products

Support

Reach out to our support team for help and advice

Status

Check on the status of any of our products

Sign InSign Up

Share your terminal as a web application with Go

  • Lanre Adelowo
March 26th, 2019
You will need Go 1.9+ set up on your machine. Git Bash or similar is required on Windows.

In this tutorial, we will explore how Pusher Channels can be used to share your terminal as a web page. If you want to play around with the code as you read this tutorial, visit this GitHub repository, which contains the final version of the code for you to check out and run locally.

A feature such as this is already available in CI servers, you already monitor in realtime the output of your build. It can also help in situations where you want a colleague to help troubleshoot an issue and don’t necessarily want to send log files back and forth, the colleague can take a look at your terminal directly instead.

Prerequisites

  • Golang (>=1.9) . A working knowledge of Go is required to follow this tutorial.
  • A Pusher Channels application. Create one here.
  • Git Bash if you are on Windows.

Building the program

An important aspect to this is implementing a Golang program that can act as a Pipe. So in short, we will be building a program that monitors the output of another program then displays it on the web UI we are going to build.

An example usage is:

    $ ./script | go run main.go

Here is an example of what we will be building:

The next step of action is to build the Golang program that will be used as pipe. To get started, we need to create a Pusher Channels application, that can be done by visiting the dashboard. You will need to click on the Create new app button to get started:

You will then be redirected to a control panel for your app where you’d be able to view the information about the app and more importantly, the authorization keys you need to connect to the application.

Once the above has been done, we will then proceed to create the actual Golang program. To do a little recap again, this program will perform two tasks:

  • Act as a pipe for another program
  • Start an HTTP server that displays the output of another program (the one being piped) in realtime.

The first thing to do is to create a new directory in your $GOPATH called pusher-channel-terminal-web-sync. That can be done with the following command:

    $ mkdir $GOPATH/github.com/pusher-tutorials/pusher-channel-terminal-web-sync

You will need to create an .env file with the following contents:

    // pusher-channel-terminal-web-sync/.env
    PUSHER_APP_ID="PUSHER_APP_ID"
    PUSHER_APP_KEY="PUSHER_APP_KEY"
    PUSHER_APP_SECRET="PUSHER_APP_SECRET"
    PUSHER_APP_CLUSTER="PUSHER_APP_CLUSTER"
    PUSHER_APP_SECURE="1"

Please remember to replace the placeholders with the original values. They can be gotten from the control panel.

The next step of action is to create a main.go file. This will house the actual code for connecting and publishing events to Pusher Channels so as to be able to show those in real time on the web.

You can create a main.go file with the following command:

    $ touch main.go

Once the file has been created, the next step is to fetch some required dependency such as Pusher’s client SDK. To do that, you will need to run the command below:

    $ go get github.com/joho/godotenv
    $ go get github.com/pusher/pusher-http-go

Once the above commands succeed, you will need to paste the following content into it:

    // pusher-channel-terminal-web-sync/main.go
    package main

    import (
            "bufio"
            "bytes"
            "flag"
            "fmt"
            "io"
            "log"
            "net/http"
            "os"
            "sync"
            "text/template"
            "time"

            "github.com/joho/godotenv"
            pusher "github.com/pusher/pusher-http-go"
    )

    const (
            channelName = "realtime-terminal"
            eventName   = "logs"
    )

    func main() {

            var httpPort = flag.Int("http.port", 1500, "Port to run HTTP server on ?")

            flag.Parse()

            info, err := os.Stdin.Stat()
            if err != nil {
                    log.Fatal(err)
            }

            if info.Mode()&os.ModeCharDevice != 0 {
                    log.Println("This command is intended to be used as a pipe such as yourprogram | thisprogram")
                    os.Exit(0)
            }

            if err := godotenv.Load(); err != nil {
                    log.Fatal("Error loading .env file")
            }

            appID := os.Getenv("PUSHER_APP_ID")
            appKey := os.Getenv("PUSHER_APP_KEY")
            appSecret := os.Getenv("PUSHER_APP_SECRET")
            appCluster := os.Getenv("PUSHER_APP_CLUSTER")
            appIsSecure := os.Getenv("PUSHER_APP_SECURE")

            var isSecure bool
            if appIsSecure == "1" {
                    isSecure = true
            }

            client := &pusher.Client{
                    AppId:   appID,
                    Key:     appKey,
                    Secret:  appSecret,
                    Cluster: appCluster,
                    Secure:  isSecure,
                    HttpClient: &http.Client{
                            Timeout: time.Minute * 2,
                    },
            }

            go func() {
                    var t *template.Template
                    var once sync.Once

                    http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("."))))

                    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {

                            once.Do(func() {
                                    tem, err := template.ParseFiles("index.html")
                                    if err != nil {
                                            log.Fatal(err)
                                    }

                                    t = tem.Lookup("index.html")
                            })

                            t.Execute(w, nil)
                    })
                    log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *httpPort), nil))
            }()

            reader := bufio.NewReader(os.Stdin)

            var writer io.Writer
            writer = pusherChannelWriter{client: client}

            for {
                    in, _, err := reader.ReadLine()
                    if err != nil && err == io.EOF {
                            break
                    }

                    in = append(in, []byte("\n")...)
                    if _, err := writer.Write(in); err != nil {
                            log.Fatalln(err)
                    }
            }
    }

    type pusherChannelWriter struct {
            client *pusher.Client
    }

    func (pusher pusherChannelWriter) Write(p []byte) (int, error) {
            s := string(p)
            dd := bytes.Split(p, []byte("\n"))

            var data = make([]string, 0, len(dd))

            for _, v := range dd {
                    data = append(data, string(v))
            }

            _, err := pusher.client.Trigger(channelName, eventName, s)
            return len(p), err
    }

While the above code is a bit lengthy, I’d break it down.

  • Line 35 - 38 is probably the most interesting part. We make sure the program can only be run if it is acting as a pipe to another program. An example is someprogram | ourprogram.
  • Line 66 - 88 is where we start the HTTP server. The server will load up an index.html file where the contents of the program we are acting as a pipe for will be seen in realtime. Maybe another interesting thing is var once sync.Once. What sync.Once offers us is the ability to perform a task just once throughout the lifetime of a program, with this we load the contents of index.html just once and don’t have to repeat it every time the web page is requested.
  • Line 109 - 125 is where we actually send output to Pusher Channels.

Great, something we have missed so far is the index.html file. You will need to go ahead to create that in the root directory with the following command:

    $ touch index.html

Open the newly created file and paste in the following contents:

    // pusher-channel-terminal-web-sync/index.html
    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Pusher realtime terminal sync</title>
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <link rel="icon" type="image/x-icon" href="favicon.ico" />
        <link href="static/app.css" rel="stylesheet">
      <body>
      <div id="terminal">
              <pre>
                <output id="logs"></output>
              </pre>
      </div>
      </body>
      <script src="https://js.pusher.com/4.3/pusher.min.js"></script>
      <script src="static/app.js"></script>
    </html>

As you may have noticed, we referenced app.js and app.css file. We will get started with the app.js file , that can be done with the following command:

    $ touch app.js

Once done, you will need to paste the following contents into it.:

    // pusher-channel-terminal-web-sync/app.js
    (function() {
      const APP_KEY = 'PUSHER_APP_KEY';
      const APP_CLUSTER = 'PUSHER_APP_CLUSTER';

      const logsDiv = document.getElementById('logs');

      const pusher = new Pusher(APP_KEY, {
        cluster: APP_CLUSTER,
      });

      const channel = pusher.subscribe('realtime-terminal');

      channel.bind('logs', data => {
        const divElement = document.createElement('div');
        divElement.innerHTML = data;

        logsDiv.appendChild(divElement);
      });
    })();

Do make sure to replace PUSHER_APP_KEY and PUSHER_APP_CLUSTER with your original credentials

You also need to create the app.css file. That can be done with:

    $ touch app.css 

Once done, paste the following contents into it:

    // pusher-channel-terminal-web-sync/app.css
    #terminal {
            font-family: courier, monospace;
            color: #fff;
            width:750px;
            margin-left:auto;
            margin-right:auto;
            margin-top:100px;
            font-size:14px;
    }

    body {
            background-color: #000
    }

Nothing too fancy right? We just make the website’s background black and try to mimic a real terminal.

All is set and we can go ahead to test our program. A major key to testing this is an application that writes to standard output, such programs like cat or a running NodeJS program that writes log to standard output.

To make this as simple as can be, we will make use of another Go program that writes a UUID to standard output every second. This file can be created with:

    # This should be done within the pusher-channel-terminal-web-sync directory
    $ mkdir uuid
    $ touch uuid/uuid.go

Since we will be generating UUIDs, we will require a dependency for that. You can install that by running:

    $ go get github.com/google/uuid

In the newly created uuid.go, paste the following contents into it:

    // pusher-channel-terminal-web-sync/uuid/uuid.go
    package main

    import (
            "fmt"
            "time"

            "github.com/google/uuid"
    )

    func main() {

            for {
                    time.Sleep(time.Millisecond * 500)
                    fmt.Printf("Generating a new UUID -- %s", uuid.New())
                    fmt.Println()
            }
    }

All is set right now for us to test. To do this, we will need to build both the UUID generator and our actual program.

    # Linux and Mac
    $ go build -o uuidgenerator uuid/uuid.go
    $ go build

    # Windows
    $ go build -o uuidgenerator.exe uuid/uuid.go
    $ go build

Once the above has been done, we will then run both of them. That can be done by running the command below:

    $ ./uuidgenerator | ./pusher-channel-terminal-web-sync

There should be no output in the terminal but you should visit http://localhost:1500 in other to view the output of the UUID generator in real time. You should be presented with something as depicted in the gif below:

Conclusion

In this tutorial, I have described how Pusher Channels can be leveraged to build a realtime view of your terminal. This can be really useful if you want to share your terminal with someone else on the same network as you are or with a tool such as ngrok. You could do something like ngrok http 1500 and share the link with someone else.

As always, you can find the code on GitHub.

Clone the project repository
  • Collaboration
  • Go
  • Channels

Products

  • Channels
  • Beams
  • Chatkit

© 2019 Pusher Ltd. All rights reserved.

Pusher Limited is a company registered in England and Wales (No. 07489873) whose registered office is at 160 Old Street, London, EC1V 9BW.