Initial Release
This commit is contained in:
Vendored
+88
@@ -0,0 +1,88 @@
|
||||
package env
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const (
|
||||
MAX_FORM_BYTES = 1 << 24
|
||||
FILE_MODE = os.FileMode(0770)
|
||||
STICKERBOARD_FILENAME = "stickerboard.webp"
|
||||
)
|
||||
|
||||
var (
|
||||
HTTP_TLS *tls.Config // http: TLS Configuration
|
||||
HTTP_PROXY_HEADER = envString("HTTP_PROXY_HEADER", "") // http: Retrieve IP Address from Following HTTP Header
|
||||
HTTP_ADDRESS = envString("HTTP_ADDRESS", "localhost:8080") // http: Address to Listen for Requests on
|
||||
TLS_ENABLED = envString("TLS_ENABLED", "false") == "true" // http: Enable TLS?
|
||||
TLS_CERT = envString("TLS_CERT", "tls_crt.pem") // http: Path to TLS Certificate
|
||||
TLS_KEY = envString("TLS_KEY", "tls_key.pem") // http: Path to TLS Key
|
||||
TLS_CA = envString("TLS_CA", "tls_ca.pem") // http: Path to TLS CA Bundle
|
||||
DATA_DIRECTORY = envString("DATA_DIRECTORY", "data") // env: Data Directory
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Create Data Directory
|
||||
if err := os.MkdirAll(DATA_DIRECTORY, FILE_MODE); err != nil {
|
||||
log.Fatalln("[env/data] Create Directory Error:", err)
|
||||
}
|
||||
|
||||
// Load and Parse TLS Configuration from Disk
|
||||
if TLS_ENABLED {
|
||||
cert, err := tls.LoadX509KeyPair(TLS_CERT, TLS_KEY)
|
||||
if err != nil {
|
||||
log.Fatalln("[env/tls] Cannot Load Keypair", err)
|
||||
}
|
||||
caBytes, err := os.ReadFile(TLS_CA)
|
||||
if err != nil {
|
||||
log.Fatalln("[env/tls] Cannot Read CA File", err)
|
||||
}
|
||||
caPool := x509.NewCertPool()
|
||||
if !caPool.AppendCertsFromPEM(caBytes) {
|
||||
log.Fatalln("[env/tls] Cannot Append Certificates")
|
||||
}
|
||||
HTTP_TLS = &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
ClientCAs: caPool,
|
||||
MinVersion: tls.VersionTLS13,
|
||||
MaxVersion: tls.VersionTLS13,
|
||||
CipherSuites: []uint16{
|
||||
tls.TLS_AES_128_GCM_SHA256,
|
||||
tls.TLS_AES_256_GCM_SHA384,
|
||||
tls.TLS_CHACHA20_POLY1305_SHA256,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reads String from Environment
|
||||
func envString(key, defaultValue string) string {
|
||||
systemValue := os.Getenv(key)
|
||||
if systemValue == "" {
|
||||
if defaultValue == "\x00" {
|
||||
fmt.Printf("[env] Environment Variable '%s' is undefined\n", key)
|
||||
os.Exit(2)
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
return systemValue
|
||||
}
|
||||
|
||||
// Read Number from Environment
|
||||
func envNumber(key string, defaultValue int) int {
|
||||
systemValue := os.Getenv(key)
|
||||
if systemValue == "" {
|
||||
return defaultValue
|
||||
}
|
||||
v, err := strconv.Atoi(systemValue)
|
||||
if err != nil {
|
||||
fmt.Printf("[env] Environment Variable '%s' is not a integer: %s\n", key, err)
|
||||
os.Exit(2)
|
||||
}
|
||||
return v
|
||||
}
|
||||
Vendored
+75
@@ -0,0 +1,75 @@
|
||||
package env
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type DatabaseRoot struct {
|
||||
Stickers []DatabaseSticker `json:"stickers"`
|
||||
}
|
||||
|
||||
type DatabaseSticker struct {
|
||||
Created time.Time `json:"created"` // Sticker Created
|
||||
UserAddress string `json:"user_address"` // User IP Address (For Manual Bans)
|
||||
UserName string `json:"user_name"` // User Name
|
||||
UserURL string `json:"user_url"` // User URL (Optional)
|
||||
Message string `json:"message"` // Sticker Message
|
||||
Visible bool `json:"visible"` // Sticker Visible?
|
||||
OffsetX int `json:"offset_x"` // Placement X
|
||||
OffsetY int `json:"offset_y"` // Placement Y
|
||||
ImageScale float64 `json:"image_scale"` // Image Scale
|
||||
ImageHeight int `json:"image_height"` // Image Height
|
||||
ImageWidth int `json:"image_width"` // Image Width
|
||||
ImageType ImageType `json:"image_type"` // Original Image File Type
|
||||
ImageHash string `json:"image_hash"` // Original Image File Hash
|
||||
}
|
||||
|
||||
var (
|
||||
Database DatabaseRoot
|
||||
DatabaseMtx sync.RWMutex
|
||||
)
|
||||
|
||||
func SetupDatabase(stop context.Context, await *sync.WaitGroup) {
|
||||
t := time.Now()
|
||||
p := path.Join(DATA_DIRECTORY, "database.json")
|
||||
|
||||
// Decode File
|
||||
b, err := os.ReadFile(p)
|
||||
if err != nil {
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
log.Fatalln("[db] Read Database Error:", err)
|
||||
}
|
||||
Database.Stickers = make([]DatabaseSticker, 0)
|
||||
}
|
||||
if err == nil {
|
||||
if err := json.Unmarshal(b, &Database); err != nil {
|
||||
log.Fatalln("[db] Parse Database Error:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Shutdown Logic
|
||||
await.Add(1)
|
||||
go func() {
|
||||
defer await.Done()
|
||||
<-stop.Done()
|
||||
|
||||
// Write Database To Disk
|
||||
b, err := json.MarshalIndent(&Database, "", " ")
|
||||
if err != nil {
|
||||
log.Fatalln("[db] Cannot Marshal Database:", err)
|
||||
}
|
||||
if err := os.WriteFile(p, b, FILE_MODE); err != nil {
|
||||
log.Fatalln("[db] Cannot Write Database:", err)
|
||||
}
|
||||
log.Println("[db] Database Saved")
|
||||
}()
|
||||
|
||||
log.Println("[db] Ready in", time.Since(t))
|
||||
}
|
||||
Vendored
+101
@@ -0,0 +1,101 @@
|
||||
package env
|
||||
|
||||
import (
|
||||
"context"
|
||||
"image"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
tf "github.com/galeone/tensorflow/tensorflow/go"
|
||||
"golang.org/x/image/draw"
|
||||
)
|
||||
|
||||
const (
|
||||
MODEL_TRESHOLD = 0.7 // Threshold before an image is considered inappropriate
|
||||
MODEL_SIZE = 224 // Model Size
|
||||
)
|
||||
|
||||
var nsfwModel *tf.SavedModel
|
||||
|
||||
func SetupModel(stop context.Context, await *sync.WaitGroup) {
|
||||
t := time.Now()
|
||||
|
||||
// Read Model from Disk
|
||||
model, err := tf.LoadSavedModel("resources/model", []string{"serve"}, nil)
|
||||
if err != nil {
|
||||
log.Fatalf("[model] Unable to Load Model (%s)\n", err)
|
||||
}
|
||||
nsfwModel = model
|
||||
|
||||
// Test Model using Dummy Tensor
|
||||
dummy, _ := tf.NewTensor([1][MODEL_SIZE][MODEL_SIZE][3]float32{})
|
||||
if _, err := ModelClassifyTensor(dummy); err != nil {
|
||||
log.Fatalf("[model] Failed to Initialize Model (%s)\n", err)
|
||||
}
|
||||
|
||||
// Shutdown Logic
|
||||
await.Add(1)
|
||||
go func() {
|
||||
defer await.Done()
|
||||
<-stop.Done()
|
||||
nsfwModel.Session.Close()
|
||||
log.Println("[model] Model Closed")
|
||||
}()
|
||||
|
||||
log.Printf("[model] Model Ready (%s)\n", time.Since(t))
|
||||
}
|
||||
|
||||
// Cast Predictions on a Tensor using the NSFW Model
|
||||
func ModelClassifyTensor(tensor *tf.Tensor) ([]float32, error) {
|
||||
results, err := nsfwModel.Session.Run(
|
||||
map[tf.Output]*tf.Tensor{
|
||||
nsfwModel.Graph.Operation("serving_default_input").Output(0): tensor,
|
||||
},
|
||||
[]tf.Output{
|
||||
nsfwModel.Graph.Operation("StatefulPartitionedCall").Output(0),
|
||||
},
|
||||
[]*tf.Operation{},
|
||||
)
|
||||
if err != nil {
|
||||
return []float32{}, err
|
||||
}
|
||||
// cursed...
|
||||
return results[0].Value().([][]float32)[0], err
|
||||
}
|
||||
|
||||
// Classify an Image returning true if it's considered safe
|
||||
func ModelClassifyImage(someImage image.Image) (bool, error) {
|
||||
|
||||
// Resize Image to Usable Size
|
||||
resized := image.NewRGBA(image.Rect(0, 0, MODEL_SIZE, MODEL_SIZE))
|
||||
draw.NearestNeighbor.Scale(resized, resized.Rect, someImage, someImage.Bounds(), draw.Over, nil)
|
||||
|
||||
// Convert Pixel Data into Normalized Floats
|
||||
var tensorCap = MODEL_SIZE * MODEL_SIZE * 3
|
||||
var tensorData = make([]float32, 0, tensorCap)
|
||||
var tensorShape = []int64{1, MODEL_SIZE, MODEL_SIZE, 3}
|
||||
for x := 0; x < MODEL_SIZE; x++ {
|
||||
for y := 0; y < MODEL_SIZE; y++ {
|
||||
r, g, b, _ := resized.At(x, y).RGBA()
|
||||
tensorData = append(tensorData, float32(r)/65535, float32(g)/65535, float32(b)/65535)
|
||||
}
|
||||
}
|
||||
|
||||
// Create Tensor, reshape it, then classify
|
||||
tensor, err := tf.NewTensor(tensorData)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := tensor.Reshape(tensorShape); err != nil {
|
||||
return false, err
|
||||
}
|
||||
results, err := ModelClassifyTensor(tensor)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Calculate How Inappropriate this Image is
|
||||
// Drawing[0], Hentai[1], Neutral[2], Porn[3], Sexy[4]
|
||||
return (results[1] + results[3] + (results[4] * 0.9)) < MODEL_TRESHOLD, nil
|
||||
}
|
||||
Vendored
+264
@@ -0,0 +1,264 @@
|
||||
package env
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/gif"
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"runtime"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"golang.org/x/image/draw"
|
||||
"golang.org/x/image/webp"
|
||||
)
|
||||
|
||||
const (
|
||||
CANVAS_WIDTH = 854 // Canvas Width
|
||||
CANVAS_HEIGHT = 480 // Canvas Height
|
||||
CANVAS_STICKER_MAX_HEIGHT = 240 // Canvas Max Sticker Height in Pixels
|
||||
CANVAS_FRAMES = 100
|
||||
CANVAS_FPS = 20
|
||||
CANVAS_DELAY = 100 / CANVAS_FPS
|
||||
)
|
||||
|
||||
var (
|
||||
StickerboardReady atomic.Bool
|
||||
StickerboardPath = path.Join(DATA_DIRECTORY, STICKERBOARD_FILENAME)
|
||||
StickerboardBack *image.RGBA
|
||||
StickerboardMtx sync.RWMutex
|
||||
Stickerboard []byte
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Initialize Background to Black to Prevent Coalescing
|
||||
base := image.NewRGBA(image.Rect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT))
|
||||
for x := 0; x < CANVAS_WIDTH; x++ {
|
||||
for y := 0; y < CANVAS_HEIGHT; y++ {
|
||||
base.Set(x, y, color.Black)
|
||||
}
|
||||
}
|
||||
StickerboardBack = base
|
||||
|
||||
// Load Custom Background (if any)
|
||||
p := path.Join(DATA_DIRECTORY, "background.png")
|
||||
if f, err := os.Open(p); err == nil {
|
||||
if o, err := png.Decode(f); err == nil {
|
||||
b := base.Bounds()
|
||||
draw.CatmullRom.Scale(base, b.Bounds(), o, o.Bounds(), draw.Over, nil)
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func stickerboardCopy() {
|
||||
b, err := os.ReadFile(StickerboardPath)
|
||||
if err != nil {
|
||||
log.Println("[stickerboard] Read Image Error:", err)
|
||||
return
|
||||
}
|
||||
StickerboardMtx.Lock()
|
||||
Stickerboard = b
|
||||
StickerboardReady.Store(true)
|
||||
StickerboardMtx.Unlock()
|
||||
}
|
||||
|
||||
func StickerboardRender() (int, error) {
|
||||
t := time.Now()
|
||||
|
||||
// Mass Decode and Resizing of all Stickers
|
||||
type DecodedSticker struct {
|
||||
Position image.Rectangle
|
||||
Frames []*image.RGBA
|
||||
Delays []int
|
||||
}
|
||||
stickers := make([]DecodedSticker, len(Database.Stickers))
|
||||
|
||||
DatabaseMtx.RLock()
|
||||
if err := Multithread(len(stickers), func(i int) error {
|
||||
|
||||
// Read Sticker from Disk
|
||||
info := &Database.Stickers[i]
|
||||
path := path.Join(DATA_DIRECTORY, info.ImageHash)
|
||||
reader, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer reader.Close()
|
||||
var images = make([]image.Image, 1)
|
||||
var delays = make([]int, 1)
|
||||
|
||||
// Decode Sticker Frames
|
||||
var decodeGIF *gif.GIF
|
||||
var decodeImage image.Image
|
||||
var decodeError error
|
||||
switch info.ImageType {
|
||||
case IMAGE_GIF:
|
||||
decodeGIF, decodeError = gif.DecodeAll(reader)
|
||||
case IMAGE_WEBP:
|
||||
decodeImage, decodeError = webp.Decode(reader)
|
||||
case IMAGE_JPEG:
|
||||
decodeImage, decodeError = jpeg.Decode(reader)
|
||||
case IMAGE_PNG:
|
||||
decodeImage, decodeError = png.Decode(reader)
|
||||
default:
|
||||
return fmt.Errorf("decoder available")
|
||||
}
|
||||
if decodeError != nil {
|
||||
return err
|
||||
}
|
||||
if info.ImageType == IMAGE_GIF {
|
||||
|
||||
// This section here properly layers GIF frames
|
||||
|
||||
delays = decodeGIF.Delay
|
||||
images = make([]image.Image, len(decodeGIF.Image))
|
||||
|
||||
var imageBase = image.NewRGBA(image.Rect(0, 0, decodeGIF.Config.Width, decodeGIF.Config.Height))
|
||||
var imagePrev *image.RGBA
|
||||
var disposal = byte(gif.DisposalNone)
|
||||
|
||||
for i, frame := range decodeGIF.Image {
|
||||
if disposal == gif.DisposalPrevious {
|
||||
imagePrev = image.NewRGBA(imageBase.Bounds())
|
||||
copy(imageBase.Pix, imagePrev.Pix)
|
||||
}
|
||||
if i > 0 {
|
||||
switch disposal {
|
||||
case gif.DisposalBackground:
|
||||
draw.Draw(imageBase, decodeGIF.Image[i-1].Bounds(), image.Transparent, image.Point{}, draw.Src)
|
||||
case gif.DisposalPrevious:
|
||||
if imagePrev != nil {
|
||||
draw.Draw(imageBase, imageBase.Bounds(), imagePrev, image.Point{}, draw.Src)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Save Composited Frame
|
||||
draw.Draw(imageBase, frame.Bounds(), frame, frame.Bounds().Min, draw.Over)
|
||||
imageCopy := image.NewRGBA(imageBase.Bounds())
|
||||
copy(imageCopy.Pix, imageBase.Pix)
|
||||
|
||||
images[i] = imageCopy
|
||||
disposal = decodeGIF.Disposal[i]
|
||||
}
|
||||
|
||||
} else {
|
||||
// Copy Static Frame
|
||||
images[0] = decodeImage
|
||||
}
|
||||
|
||||
// Resize Decoded Frames
|
||||
var (
|
||||
stickerFrames = make([]*image.RGBA, len(images))
|
||||
stickerPosition image.Rectangle
|
||||
stickerBounds = images[0].Bounds()
|
||||
stickerWidth = int(float64(stickerBounds.Dx()) * info.ImageScale)
|
||||
stickerHeight = int(float64(stickerBounds.Dy()) * info.ImageScale)
|
||||
)
|
||||
for j := range images {
|
||||
// Nice and Smooth Scaling
|
||||
source := images[j]
|
||||
scaled := image.NewRGBA(image.Rect(0, 0, stickerWidth, stickerHeight))
|
||||
draw.CatmullRom.Scale(scaled, scaled.Bounds(), source, source.Bounds(), draw.Over, nil)
|
||||
|
||||
// Invert Y position because browsers placment origin is bottom-left but server is top-left
|
||||
y := CANVAS_HEIGHT - info.OffsetY - stickerHeight
|
||||
stickerPosition = image.Rect(info.OffsetX, y, info.OffsetX+stickerWidth, y+stickerHeight)
|
||||
stickerFrames[j] = scaled
|
||||
}
|
||||
|
||||
// Store Resized Frame
|
||||
stickers[i] = DecodedSticker{
|
||||
Frames: stickerFrames,
|
||||
Delays: delays,
|
||||
Position: stickerPosition,
|
||||
}
|
||||
return nil
|
||||
|
||||
}); err != nil {
|
||||
DatabaseMtx.RUnlock()
|
||||
return 0, err
|
||||
}
|
||||
DatabaseMtx.RUnlock()
|
||||
|
||||
// Startup Encoder
|
||||
var outputLogs bytes.Buffer
|
||||
cmd := exec.Command(
|
||||
"ffmpeg", "-y",
|
||||
"-threads", fmt.Sprint(runtime.NumCPU()),
|
||||
"-f", "rawvideo",
|
||||
"-pix_fmt", "rgba",
|
||||
"-s", fmt.Sprintf("%dx%d", CANVAS_WIDTH, CANVAS_HEIGHT),
|
||||
"-framerate", fmt.Sprint(CANVAS_FPS),
|
||||
"-i", "pipe:0",
|
||||
"-vcodec", "libwebp",
|
||||
"-compression_level", "4",
|
||||
"-q:v", "75",
|
||||
"-loop", "0",
|
||||
StickerboardPath,
|
||||
)
|
||||
cmdStdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
cmd.Stdout = &outputLogs
|
||||
cmd.Stderr = &outputLogs
|
||||
if err := cmd.Start(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Generate Frames
|
||||
for i := 0; i < CANVAS_FRAMES; i++ {
|
||||
|
||||
// Generate Frame
|
||||
canvas := image.NewRGBA(StickerboardBack.Rect)
|
||||
copy(canvas.Pix, StickerboardBack.Pix)
|
||||
for j := range stickers {
|
||||
decode := &stickers[j]
|
||||
index := 0
|
||||
if len(decode.Frames) > 1 {
|
||||
offset := 0
|
||||
for {
|
||||
idx := index % len(decode.Frames)
|
||||
offset += decode.Delays[idx]
|
||||
if offset > (i * CANVAS_DELAY) {
|
||||
index = idx
|
||||
break
|
||||
}
|
||||
index++
|
||||
}
|
||||
}
|
||||
draw.Draw(canvas, decode.Position, decode.Frames[index], image.Point{}, draw.Over)
|
||||
}
|
||||
|
||||
// Submit Encoder
|
||||
for y := 0; y < canvas.Rect.Dy(); y++ {
|
||||
start := y * canvas.Stride
|
||||
end := start + canvas.Rect.Dx()*4
|
||||
cmdStdin.Write(canvas.Pix[start:end])
|
||||
}
|
||||
}
|
||||
|
||||
// Await Encoding
|
||||
cmdStdin.Close()
|
||||
if err := cmd.Wait(); err != nil {
|
||||
exitCode := -1
|
||||
if cmd.ProcessState != nil {
|
||||
exitCode = cmd.ProcessState.ExitCode()
|
||||
}
|
||||
log.Println("[sticker] FFMPEG Exited With Code %d\n%s\n", exitCode, outputLogs.String())
|
||||
return 0, err
|
||||
}
|
||||
|
||||
log.Printf("[sticker] Rendered %d Images in %s", len(stickers), time.Since(t))
|
||||
stickerboardCopy()
|
||||
return len(stickers), nil
|
||||
}
|
||||
Vendored
+84
@@ -0,0 +1,84 @@
|
||||
package env
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
type ImageType string
|
||||
|
||||
const (
|
||||
IMAGE_OTHER ImageType = "UNKNOWN"
|
||||
IMAGE_WEBP ImageType = "WEBP"
|
||||
IMAGE_JPEG ImageType = "JPG"
|
||||
IMAGE_PNG ImageType = "PNG"
|
||||
IMAGE_GIF ImageType = "GIF"
|
||||
)
|
||||
|
||||
// Classify the contents of a file based on it's starting bytes
|
||||
// https://en.wikipedia.org/wiki/Magic_number_(programming)#Magic_numbers_in_files)
|
||||
func ImageSniffType(d []byte) ImageType {
|
||||
switch {
|
||||
case len(d) > 3 && // JPEG
|
||||
d[0] == 0xFF && d[1] == 0xD8 && d[2] == 0xFF:
|
||||
return IMAGE_JPEG
|
||||
|
||||
case len(d) > 8 && // PNG
|
||||
d[0] == 0x89 && d[1] == 0x50 && d[2] == 0x4E && d[3] == 0x47 &&
|
||||
d[4] == 0x0D && d[5] == 0x0A && d[6] == 0x1A && d[7] == 0x0A:
|
||||
return IMAGE_PNG
|
||||
|
||||
case len(d) > 4 && // GIF
|
||||
d[0] == 0x47 && d[1] == 0x49 && d[2] == 0x46 && d[3] == 0x38:
|
||||
return IMAGE_GIF
|
||||
|
||||
case len(d) > 12 && // WEBP
|
||||
d[0] == 0x52 && d[1] == 0x49 && d[2] == 0x46 && d[3] == 0x46 &&
|
||||
d[8] == 0x57 && d[9] == 0x45 && d[10] == 0x42 && d[11] == 0x50:
|
||||
return IMAGE_WEBP
|
||||
|
||||
default:
|
||||
return IMAGE_OTHER
|
||||
}
|
||||
}
|
||||
|
||||
// Easily spread a workload across all available threads, returning the error if any
|
||||
func Multithread(jobCount int, handler func(i int) error) error {
|
||||
threads := runtime.NumCPU()
|
||||
channel := make(chan int, jobCount)
|
||||
var wait sync.WaitGroup
|
||||
var fail atomic.Value
|
||||
|
||||
// Startup Goroutines
|
||||
for workerId := 0; workerId < threads; workerId++ {
|
||||
wait.Add(1)
|
||||
go func() {
|
||||
defer wait.Done()
|
||||
for i := range channel {
|
||||
// Exit early if an error has ocurred
|
||||
if fail.Load() != nil {
|
||||
return
|
||||
}
|
||||
// Run Handler and store it's error (if any)
|
||||
if err := handler(i); err != nil {
|
||||
fail.Store(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Queue and Complete Work Across all Threads
|
||||
for i := 0; i < jobCount; i++ {
|
||||
channel <- i
|
||||
}
|
||||
close(channel) // close or wait forever
|
||||
wait.Wait()
|
||||
|
||||
// Return Error (if any)
|
||||
if v := fail.Load(); v != nil {
|
||||
return v.(error)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user