Initial commit
This commit is contained in:
commit
b7cd308016
7 changed files with 400 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
chrome-dev
|
||||||
18
go.mod
Normal file
18
go.mod
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
module chrome-dev
|
||||||
|
|
||||||
|
go 1.25.4
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/chromedp/chromedp v0.14.2
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20250724212937-08a3db8b4327 // indirect
|
||||||
|
github.com/chromedp/sysutil v1.1.0 // indirect
|
||||||
|
github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2 // indirect
|
||||||
|
github.com/gobwas/httphead v0.1.0 // indirect
|
||||||
|
github.com/gobwas/pool v0.2.1 // indirect
|
||||||
|
github.com/gobwas/ws v1.4.0 // indirect
|
||||||
|
golang.org/x/sys v0.34.0 // indirect
|
||||||
|
)
|
||||||
23
go.sum
Normal file
23
go.sum
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20250724212937-08a3db8b4327 h1:UQ4AU+BGti3Sy/aLU8KVseYKNALcX9UXY6DfpwQ6J8E=
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20250724212937-08a3db8b4327/go.mod h1:NItd7aLkcfOA/dcMXvl8p1u+lQqioRMq/SqDp71Pb/k=
|
||||||
|
github.com/chromedp/chromedp v0.14.2 h1:r3b/WtwM50RsBZHMUm9fsNhhzRStTHrKdr2zmwbZSzM=
|
||||||
|
github.com/chromedp/chromedp v0.14.2/go.mod h1:rHzAv60xDE7VNy/MYtTUrYreSc0ujt2O1/C3bzctYBo=
|
||||||
|
github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM=
|
||||||
|
github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8=
|
||||||
|
github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2 h1:iizUGZ9pEquQS5jTGkh4AqeeHCMbfbjeb0zMt0aEFzs=
|
||||||
|
github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M=
|
||||||
|
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
|
||||||
|
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
||||||
|
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
||||||
|
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||||
|
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
|
||||||
|
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
|
||||||
|
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
|
||||||
|
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
|
||||||
|
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||||
|
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
138
main.go
Normal file
138
main.go
Normal file
|
|
@ -0,0 +1,138 @@
|
||||||
|
// Command remote is a chromedp example demonstrating how to connect to an
|
||||||
|
// existing Chrome DevTools instance using a remote WebSocket URL.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
// Standard
|
||||||
|
"embed"
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"html/template"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
flagVerbose bool
|
||||||
|
flagWsURL string
|
||||||
|
flagWatch string
|
||||||
|
|
||||||
|
//go:embed static
|
||||||
|
fs embed.FS
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.BoolVar(&flagVerbose, "v", false, "verbose")
|
||||||
|
flag.StringVar(&flagWsURL, "ws", "ws://127.0.0.1:9222", "devtools url")
|
||||||
|
flag.StringVar(&flagWatch, "watch", "", "Files to watch")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
http.HandleFunc("/", pageIndex)
|
||||||
|
http.HandleFunc("/css/main.css", pageCSS)
|
||||||
|
|
||||||
|
http.HandleFunc("/start", actionStart)
|
||||||
|
http.HandleFunc("/stop/{uuid}", actionStop)
|
||||||
|
http.HandleFunc("/sites", actionSites)
|
||||||
|
|
||||||
|
log.Println("Listen on [::]:5123")
|
||||||
|
http.ListenAndServe("[::]:5123", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func pageIndex(w http.ResponseWriter, r *http.Request) { // {{{
|
||||||
|
var tmpl *template.Template
|
||||||
|
var err error
|
||||||
|
if false {
|
||||||
|
tmpl, err = template.ParseFS(fs, "static/html/index.html")
|
||||||
|
}
|
||||||
|
tmpl, err = template.ParseFiles("static/html/index.html")
|
||||||
|
if err != nil {
|
||||||
|
w.Write([]byte(err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpl.Execute(w, sites)
|
||||||
|
} // }}}
|
||||||
|
func pageCSS(w http.ResponseWriter, r *http.Request) { // {{{
|
||||||
|
w.Header().Add("Content-Type", "text/css")
|
||||||
|
|
||||||
|
//data, err := fs.ReadFile("static/css/main.css")
|
||||||
|
data, err := os.ReadFile("static/css/main.css")
|
||||||
|
if err != nil {
|
||||||
|
w.Write([]byte(err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Write(data)
|
||||||
|
} // }}}
|
||||||
|
func actionSites(w http.ResponseWriter, r *http.Request) { // {{{
|
||||||
|
j, _ := json.Marshal(struct {
|
||||||
|
OK bool
|
||||||
|
Sites map[string]*Site
|
||||||
|
}{
|
||||||
|
true,
|
||||||
|
sites,
|
||||||
|
})
|
||||||
|
w.Write(j)
|
||||||
|
} // }}}
|
||||||
|
|
||||||
|
func actionStart(w http.ResponseWriter, r *http.Request) { // {{{
|
||||||
|
var req struct {
|
||||||
|
URL string
|
||||||
|
Watch string
|
||||||
|
}
|
||||||
|
|
||||||
|
body, _ := io.ReadAll(r.Body)
|
||||||
|
err := json.Unmarshal(body, &req)
|
||||||
|
if err != nil {
|
||||||
|
j, _ := json.Marshal(struct {
|
||||||
|
OK bool
|
||||||
|
Error string
|
||||||
|
}{
|
||||||
|
false,
|
||||||
|
err.Error(),
|
||||||
|
})
|
||||||
|
w.Write(j)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var site Site
|
||||||
|
site, err = NewSite(flagWsURL, req.URL, req.Watch)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
j, _ := json.Marshal(struct {
|
||||||
|
OK bool
|
||||||
|
Site Site
|
||||||
|
}{
|
||||||
|
true,
|
||||||
|
site,
|
||||||
|
})
|
||||||
|
w.Write(j)
|
||||||
|
} // }}}
|
||||||
|
func actionStop(w http.ResponseWriter, r *http.Request) { // {{{
|
||||||
|
siteUUID := r.PathValue("uuid")
|
||||||
|
|
||||||
|
site, found := sites[siteUUID]
|
||||||
|
if !found {
|
||||||
|
j, _ := json.Marshal(struct {
|
||||||
|
OK bool
|
||||||
|
Error string
|
||||||
|
}{
|
||||||
|
false,
|
||||||
|
"Site not found",
|
||||||
|
})
|
||||||
|
w.Write(j)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
site.StopLoop = true
|
||||||
|
|
||||||
|
j, _ := json.Marshal(struct {
|
||||||
|
OK bool
|
||||||
|
}{
|
||||||
|
true,
|
||||||
|
})
|
||||||
|
w.Write(j)
|
||||||
|
} // }}}
|
||||||
117
site.go
Normal file
117
site.go
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
// External
|
||||||
|
"github.com/chromedp/cdproto/target"
|
||||||
|
"github.com/chromedp/chromedp"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
|
||||||
|
// Standard
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os/exec"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
sites map[string]*Site
|
||||||
|
siteLock *sync.Mutex
|
||||||
|
)
|
||||||
|
|
||||||
|
type Site struct {
|
||||||
|
UUID string
|
||||||
|
URL string
|
||||||
|
Watch string
|
||||||
|
WatchLoop *exec.Cmd `json:"-"`
|
||||||
|
StopLoop bool
|
||||||
|
Context context.Context `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
siteLock = &sync.Mutex{}
|
||||||
|
sites = make(map[string]*Site)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSite(wsURL, url, watch string) (s Site, err error) {
|
||||||
|
s.UUID = uuid.NewString()
|
||||||
|
s.URL = url
|
||||||
|
s.Watch = watch
|
||||||
|
|
||||||
|
allocatorContext, _ := chromedp.NewRemoteAllocator(context.Background(), wsURL)
|
||||||
|
|
||||||
|
// Used to optionally enable debugging output.
|
||||||
|
var opts []chromedp.ContextOption
|
||||||
|
if flagVerbose {
|
||||||
|
opts = append(opts, chromedp.WithDebugf(log.Printf))
|
||||||
|
}
|
||||||
|
s.Context, _ = chromedp.NewContext(allocatorContext, opts...)
|
||||||
|
|
||||||
|
// A tab is started up and navigated to in order to get a context for this instance.
|
||||||
|
if err = chromedp.Run(
|
||||||
|
s.Context,
|
||||||
|
chromedp.Navigate(url),
|
||||||
|
); err != nil {
|
||||||
|
err = fmt.Errorf("Failed getting body of %s: %v", url, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
siteLock.Lock()
|
||||||
|
sites[s.UUID] = &s
|
||||||
|
defer siteLock.Unlock()
|
||||||
|
|
||||||
|
// chromedp.Run crashes something really hard and undetectable with recover().
|
||||||
|
// ListenBrowser gives the possibility to cancel the watch loop.
|
||||||
|
chromedp.ListenBrowser(s.Context, func(ev any) {
|
||||||
|
_, event1 := ev.(*target.EventDetachedFromTarget)
|
||||||
|
_, event2 := ev.(*target.EventTargetCrashed)
|
||||||
|
_, event3 := ev.(*target.EventTargetDestroyed)
|
||||||
|
if event1 || event2 || event3 {
|
||||||
|
log.Printf("Stopping loop for %s\n", s.UUID)
|
||||||
|
s.StopLoop = true
|
||||||
|
if s.WatchLoop != nil {
|
||||||
|
log.Printf("Killing inotifywait for %s\n", s.UUID)
|
||||||
|
s.WatchLoop.Process.Kill()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Start watching file/dir for changes.
|
||||||
|
go s.watchLoop()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Site) watchLoop() {
|
||||||
|
for !s.StopLoop {
|
||||||
|
log.Println("Starting watching " + s.Watch)
|
||||||
|
s.WatchLoop = exec.Command("inotifywait", "-e", "close_write", s.Watch)
|
||||||
|
if err := s.WatchLoop.Run(); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
s.ReloadCSS()
|
||||||
|
}
|
||||||
|
log.Printf("Stopping watch loop [%s]\n", s.UUID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Site) ReloadCSS() {
|
||||||
|
var buf []byte
|
||||||
|
|
||||||
|
cssReloadScript := chromedp.Evaluate(`
|
||||||
|
(()=>{
|
||||||
|
const stylesheets = document.querySelectorAll('link[rel="stylesheet"]')
|
||||||
|
for (const ss of stylesheets) {
|
||||||
|
const url = URL.parse(ss.href, location.protocol + '//' + location.host)
|
||||||
|
const nextReloadCounter = parseInt(url.searchParams.get('reload') || 0) + 1
|
||||||
|
url.searchParams.set('reload', nextReloadCounter)
|
||||||
|
ss.href = url.toString()
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
`, &buf)
|
||||||
|
|
||||||
|
err := chromedp.Run(s.Context, cssReloadScript)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
40
static/css/main.css
Normal file
40
static/css/main.css
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
body {
|
||||||
|
font-family: sans-serif;
|
||||||
|
margin: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new {
|
||||||
|
width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sites {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, min-content);
|
||||||
|
grid-gap: 8px 16px;
|
||||||
|
margin-top: 32px;
|
||||||
|
|
||||||
|
div {
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&.dead {
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
border-bottom: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stop {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
63
static/html/index.html
Normal file
63
static/html/index.html
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="initial-scale=1.0, user-scalable=no" />
|
||||||
|
<link rel="stylesheet" type="text/css" href="/css/main.css">
|
||||||
|
</head>
|
||||||
|
<script>
|
||||||
|
function newSite() {
|
||||||
|
const url = document.querySelector('.new.url').value
|
||||||
|
const watch = document.querySelector('.new.watch').value
|
||||||
|
fetch('/start', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({url, watch}),
|
||||||
|
})
|
||||||
|
.then(() => location.reload())
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopSite(uuid) {
|
||||||
|
fetch(`/stop/${uuid}`)
|
||||||
|
.then(() => location.reload())
|
||||||
|
}
|
||||||
|
|
||||||
|
function siteStatus() {
|
||||||
|
fetch('/sites')
|
||||||
|
.then(data => data.json())
|
||||||
|
.then(json => {
|
||||||
|
for (const uuid of Object.keys(json.Sites)) {
|
||||||
|
const site = json.Sites[uuid]
|
||||||
|
if (site.StopLoop) {
|
||||||
|
document.querySelector(`.url[data-uuid="${uuid}"]`)?.classList.add('dead')
|
||||||
|
document.querySelector(`.watch[data-uuid="${uuid}"]`)?.classList.add('dead')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
setInterval(siteStatus, 2000)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<input type="text" class="new url" placeholder="https://example.com" value="https://scan.euterm.n44.se">
|
||||||
|
<input type="text" class="new watch" placeholder="~/example.com/css/"
|
||||||
|
value="/home/magnus/repo/euterm/euscan/static/css">
|
||||||
|
<button onclick="newSite()">Start</button>
|
||||||
|
|
||||||
|
<div class="sites">
|
||||||
|
<div class="header">UUID</div>
|
||||||
|
<div class="header">URL</div>
|
||||||
|
<div class="header"></div>
|
||||||
|
|
||||||
|
{{ range . }}
|
||||||
|
<div class="line"></div>
|
||||||
|
<div class="url {{ if .StopLoop }}dead{{ end }}" data-uuid="{{ .UUID }}">{{ .URL }}</div>
|
||||||
|
<div class="watch {{ if .StopLoop }}dead{{ end }}" data-uuid="{{ .UUID }}">{{ .Watch }}</div>
|
||||||
|
<div class="stop" onclick="stopSite('{{ .UUID }}')">❌</div>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue