210 lines
5.7 KiB
Go
210 lines
5.7 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"net/netip"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"time"
|
|
"unsafe"
|
|
|
|
"alice-back/station/pcaudio"
|
|
"golang.org/x/sys/windows"
|
|
)
|
|
|
|
const agentLease = 5 * time.Second
|
|
|
|
type sender struct {
|
|
process windows.Handle
|
|
job windows.Handle
|
|
}
|
|
|
|
func startSender(path, host string, port uint16) (*sender, error) {
|
|
job, err := windows.CreateJobObject(nil, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var limits windows.JOBOBJECT_EXTENDED_LIMIT_INFORMATION
|
|
limits.BasicLimitInformation.LimitFlags = windows.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
|
|
if _, err := windows.SetInformationJobObject(job, windows.JobObjectExtendedLimitInformation, uintptr(unsafe.Pointer(&limits)), uint32(unsafe.Sizeof(limits))); err != nil {
|
|
windows.CloseHandle(job)
|
|
return nil, err
|
|
}
|
|
|
|
program, _ := windows.UTF16PtrFromString(path)
|
|
args, _ := windows.UTF16PtrFromString(windows.ComposeCommandLine([]string{path, host, strconv.Itoa(int(port))}))
|
|
dir, _ := windows.UTF16PtrFromString(filepath.Dir(path))
|
|
info := windows.StartupInfo{Cb: uint32(unsafe.Sizeof(windows.StartupInfo{}))}
|
|
var child windows.ProcessInformation
|
|
if err := windows.CreateProcess(program, args, nil, nil, false, windows.CREATE_SUSPENDED|windows.CREATE_NO_WINDOW, nil, dir, &info, &child); err != nil {
|
|
windows.CloseHandle(job)
|
|
return nil, err
|
|
}
|
|
defer windows.CloseHandle(child.Thread)
|
|
err = windows.AssignProcessToJobObject(job, child.Process)
|
|
if err == nil {
|
|
_, err = windows.ResumeThread(child.Thread)
|
|
}
|
|
if err != nil {
|
|
_ = windows.TerminateProcess(child.Process, 1)
|
|
windows.CloseHandle(child.Process)
|
|
windows.CloseHandle(job)
|
|
return nil, err
|
|
}
|
|
return &sender{process: child.Process, job: job}, nil
|
|
}
|
|
|
|
func (s *sender) stop() {
|
|
windows.CloseHandle(s.job)
|
|
_, _ = windows.WaitForSingleObject(s.process, 1000)
|
|
windows.CloseHandle(s.process)
|
|
}
|
|
|
|
func (s *sender) exited() bool {
|
|
status, err := windows.WaitForSingleObject(s.process, 0)
|
|
return err != nil || status != uint32(windows.WAIT_TIMEOUT)
|
|
}
|
|
|
|
func poll(client *http.Client, url, token string, state pcaudio.State) (pcaudio.Command, error) {
|
|
data, _ := json.Marshal(state)
|
|
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(data))
|
|
if err != nil {
|
|
return pcaudio.Command{}, err
|
|
}
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return pcaudio.Command{}, err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
return pcaudio.Command{}, fmt.Errorf("station returned HTTP %d", resp.StatusCode)
|
|
}
|
|
var command pcaudio.Command
|
|
err = json.NewDecoder(io.LimitReader(resp.Body, 1024)).Decode(&command)
|
|
if err == nil && len(command.Revision) != 32 {
|
|
err = errors.New("invalid station command")
|
|
}
|
|
return command, err
|
|
}
|
|
|
|
func run() error {
|
|
exe, err := os.Executable()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
dir := filepath.Dir(exe)
|
|
path := filepath.Join(dir, "ScreamSender.exe")
|
|
if _, err := os.Stat(path); err != nil {
|
|
return err
|
|
}
|
|
data, err := os.ReadFile(filepath.Join(dir, "agent.json"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var config struct {
|
|
Station string
|
|
Port uint16
|
|
Token string
|
|
}
|
|
if err := json.Unmarshal(data, &config); err != nil {
|
|
return err
|
|
}
|
|
host, err := netip.ParseAddrPort(config.Station)
|
|
if err != nil || !host.Addr().Is4() || config.Port == 0 || len(config.Token) != 64 {
|
|
return errors.New("invalid agent configuration")
|
|
}
|
|
name, _ := windows.UTF16PtrFromString("Local\\AlicePCAudio")
|
|
mutex, err := windows.CreateMutex(nil, false, name)
|
|
if mutex != 0 {
|
|
defer windows.CloseHandle(mutex)
|
|
}
|
|
if errors.Is(err, windows.ERROR_ALREADY_EXISTS) {
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
client := &http.Client{
|
|
Timeout: 900 * time.Millisecond,
|
|
Transport: &http.Transport{DialContext: (&net.Dialer{}).DialContext},
|
|
CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse },
|
|
}
|
|
url := "http://" + host.String() + pcaudio.Path
|
|
var state pcaudio.State
|
|
var child *sender
|
|
var lastSuccess time.Time
|
|
var previousError string
|
|
for {
|
|
if child != nil && child.exited() {
|
|
child.stop()
|
|
child = nil
|
|
state.Enabled = false
|
|
state.Error = "sender_exited"
|
|
log.Print("Scream Sender exited")
|
|
}
|
|
command, err := poll(client, url, config.Token, state)
|
|
now := time.Now()
|
|
if err != nil {
|
|
if err.Error() != previousError {
|
|
log.Printf("Station: %v", err)
|
|
previousError = err.Error()
|
|
}
|
|
if child != nil && now.Sub(lastSuccess) >= agentLease {
|
|
child.stop()
|
|
child = nil
|
|
state = pcaudio.State{}
|
|
log.Print("Station connection lost. Streaming stopped")
|
|
}
|
|
} else {
|
|
if lastSuccess.IsZero() || previousError != "" {
|
|
log.Print("Connected to Mini 2")
|
|
}
|
|
lastSuccess, previousError = now, ""
|
|
if command.Revision != state.Revision {
|
|
// Stop any sender from the previous command before applying a new one.
|
|
if child != nil && !command.Enabled {
|
|
child.stop()
|
|
child = nil
|
|
log.Print("Streaming stopped")
|
|
}
|
|
state = pcaudio.State{Command: command}
|
|
if command.Enabled && child == nil {
|
|
child, err = startSender(path, host.Addr().String(), config.Port)
|
|
if err != nil {
|
|
state.Enabled = false
|
|
state.Error = "sender_start_failed"
|
|
log.Printf("Scream Sender: %v", err)
|
|
} else {
|
|
log.Print("Streaming started")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
time.Sleep(650 * time.Millisecond)
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
if exe, err := os.Executable(); err == nil {
|
|
path := filepath.Join(filepath.Dir(exe), "agent.log")
|
|
if logs, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600); err == nil {
|
|
defer logs.Close()
|
|
log.SetOutput(logs)
|
|
}
|
|
}
|
|
if err := run(); err != nil {
|
|
log.Print(err)
|
|
os.Exit(1)
|
|
}
|
|
}
|