30 Commits
dev ... build

Author SHA1 Message Date
4b39024091 Update randopix.nimble 2020-05-28 21:43:47 +02:00
9b8323349c Update build.yml 2020-05-23 19:02:11 +02:00
82232f0ccf Update build.yml 2020-05-23 18:57:21 +02:00
1b82aacb7a Update build.yml 2020-05-23 15:56:26 +02:00
c43bb4c690 Update build.yml 2020-05-23 15:53:00 +02:00
e7df1883aa Update build.yml 2020-05-23 14:54:32 +02:00
4a3b9fbcd5 Update build. 2020-05-23 14:50:51 +02:00
41ea38df2e Add Github build action. 2020-05-23 14:39:59 +02:00
1a3c87bb2d Update README. 2020-05-23 14:02:29 +02:00
4683e3c5c5 Better help text for mode switching. 2020-05-23 14:00:35 +02:00
87bfce1a46 Add mode switching. 2020-05-23 13:55:16 +02:00
2907cf6574 Refactor provider module. Support starting without mode. 2020-05-23 12:42:57 +02:00
6c81d89d41 Implement inspirobot mode 2020-05-22 12:33:12 +02:00
a11f1517ff Set inital image with fixed timeout 2020-05-22 12:32:47 +02:00
ecca9e6aa6 Update README 2020-05-22 12:05:22 +02:00
a2ba7f1a37 Refresh and timeout commands reset the timer. 2020-05-22 11:57:23 +02:00
2d7a2be529 Respect the verbose verbose flag. 2020-05-21 14:22:52 +02:00
da67899921 Fix memory leak when updating images. 2020-05-21 13:52:39 +02:00
11fcf0cfc2 Add "timeout" command. 2020-05-19 12:28:49 +02:00
2a5c7ba463 Fix line endings for server. 2020-05-19 12:14:41 +02:00
8db6b5afaa Add control server. 2020-05-19 10:44:18 +02:00
54501c9390 Separate providers module. 2020-05-18 19:58:10 +02:00
4c1335378d Auto update image. 2020-05-18 18:42:57 +02:00
31014f4f14 CSS as static resource. 2020-05-16 10:55:35 +02:00
2ea3eb053e Update README 2020-05-15 21:37:05 +02:00
e7a5934e9a Add info notification. 2020-05-15 21:34:22 +02:00
9e63f77e61 Add local file provider. 2020-05-14 19:01:01 +02:00
7e07e38ab2 Add README. 2020-05-14 17:31:49 +02:00
37cb7d9299 Rename project. 2020-05-14 17:23:09 +02:00
55bc9f3211 Add argument parsing. 2020-05-14 17:19:48 +02:00
15 changed files with 666 additions and 116 deletions

30
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,30 @@
name: Build randopix
# This workflow is triggered on pushes to the repository.
on:
push:
branches:
- build
jobs:
build:
name: Build 64 Bit Linux
# This job runs on Linux
runs-on: ubuntu-latest
steps:
- name: Install GTK packages
run: sudo apt install gir1.2-gtk-3.0 gir1.2-graphene-1.0 gir1.2-gtksource-3.0 gir1.2-vte-2.91 gir1.2-notify-0.7 gir1.2-gst-plugins-bad-1.0
- uses: actions/checkout@master
- name: Cache choosenim
id: cache-choosenim
uses: actions/cache@v1
with:
path: ~/.choosenim
key: ${{ runner.os }}-choosenim-stable
- name: Cache nimble
id: cache-nimble
uses: actions/cache@v1
with:
path: ~/.nimble
key: ${{ runner.os }}-nimble-stable
- uses: jiro4989/setup-nim-action@v1.0.2
- run: nimble build -Y

13
.gitignore vendored
View File

@@ -1 +1,12 @@
bin/
# Ignore all
*
# Unignore all with extensions
!*.*
# Unignore all dirs
!*/
### Above combination will ignore all files without extension ###
bin/
.vscode/

49
README.md Normal file
View File

@@ -0,0 +1,49 @@
# randopix
## Usage
### Server
The server is run with an inital mode. All settings can later be changed with the client.
```
randopix
Version 0.1 - Display random images from different sources
Usage:
randopix [options]
Options:
-m, --mode=MODE The image source mode. Possible values: [none, foxes, inspiro, file]
-p, --path=PATH Path to a directory with images for the 'file' mode
-t, --timeout=TIMEOUT Seconds before the image is refreshed (default: 300)
-w, --windowed Do not start in fullscreen mode
-v, --verbose Show more information
-h, --help Show this help
```
### Client
The `pixctrl` client is used to issue commands to a running server.
Per default the client will try to connect to a server running on the same maschine. Use the `-s HOSTNAME` option to control a server over the network.
```
pixctrl
Control utilitiy for randopix
Usage:
pixctrl [options] COMMAND
Commands:
refresh Force image refresh now
timeout Set timeout in seconds before a new image is displayed
mode Change the display mode. Possible values: [none, foxes, inspiro, file]
Options:
-s, --server=SERVER Host running the randopix server (default: 127.0.0.1)
-p, --port=PORT Port to connect to the randopix server (default: 5555)
-h, --help Show this help
```
## Build
Install the [Nim Compiler](https://nim-lang.org/install.html).
Use this command to install the dependencies and build the program:
```
$ nimble build
```

View File

@@ -6,13 +6,13 @@ author = "luxick"
description = "Play an image slide show from different sources"
license = "GPL-2.0"
srcDir = "src"
bin = @["randopics"]
bin = @["randopix", "pixctrl"]
# Dependencies
requires "nim >= 1.0.0", "gintro <= 0.5.5"
requires "nim >= 1.0.0", "gintro <= 0.5.5", "argparse >=0.10.1"
task debug, "Compile debug version":
exec "nim c --out:bin/randopics src/randopics.nim"
exec "nim c -d:debug --debugger:native --out:randopix src/randopix.nim"
task release, "Compile release version":
exec fmt"nim c -d:release --out:bin/{version}/randopics src/randopics.nim"
exec fmt"nim c -d:release --out:randopix-{version} src/randopix.nim"

11
src/app.css Normal file
View File

@@ -0,0 +1,11 @@
window {
background: black;
font-size: 30px;
}
label {
background-color: #fff;
border: 2px solid gray;
border-radius: 5px;
margin: 10px;
padding: 10px;
}

41
src/common.nim Normal file
View File

@@ -0,0 +1,41 @@
import json
const
defaultPort* = 5555 ## Default port at which the control server will run
type
OpResult* = object of RootObj ## Result object for signalling failure state across proc calls
success*: bool ## Indicating if the opration was successfull
errorMsg*: string ## Error meassge in case the operation failed
Mode* {.pure.} = enum ## Options for the display mode
None = "none" ## No images will be displayed
Foxes = "foxes" ## Some nice foxes
Inspiro = "inspiro" ## Inspiring nonsense
File = "file" ## Images from a local path
Command* = enum
cClose = "close" ## Closes the control server and exists the applicaiton
cRefresh = "refresh" ## Force refresh of the image now
cTimeout = "timeout" ## Set image timeout to a new value
cMode = "mode" ## Change the servers display mode
CommandMessage* = object of RootObj
command*: Command ## Command that the application should execute
parameter*: string ## Optional parameter for the command
proc newOpResult*(): OpResult =
OpResult(success: true)
proc newOpResult*(msg: string): OpResult =
OpResult(success: false, errorMsg: msg)
proc newCommand*(c: Command, p: string = ""): CommandMessage =
CommandMessage(command: c, parameter: p)
proc wrap*(msg: CommandMessage): string =
$(%msg) & "\r\L"
proc enumToStrings*(en: typedesc): seq[string] =
for x in en:
result.add $x

52
src/pixctrl.nim Normal file
View File

@@ -0,0 +1,52 @@
import strutils, net
import argparse
import common
const modeHelp = "Change the display mode. Possible values: [$1]" % Mode.enumToStrings().join(", ")
var socket = newSocket()
proc sendCommand*(server, port: string, msg: CommandMessage) =
socket.connect(server, Port(port.parseInt))
if not socket.trySend(msg.wrap):
echo "Cannot send command: ", msg
socket.close()
proc switchMode*(server, port: string, mode: string) =
try:
discard parseEnum[Mode](mode)
except ValueError:
echo "Invalid mode: ", mode
echo "Possible values: [$1]" % Mode.enumToStrings().join(", ")
return
let c = newCommand(cMode, mode)
sendCommand(server, port, c)
when isMainModule:
var p = newParser("pixctrl"):
help("Control utilitiy for randopix")
option("-s", "--server", help="Host running the randopix server", default="127.0.0.1")
option("-p", "--port", help="Port to connect to the randopix server", default = $defaultPort)
command($cRefresh):
help("Force image refresh now")
run:
let c = newCommand(cRefresh)
sendCommand(opts.parentOpts.server, opts.parentOpts.port, c)
command($cTimeout):
help("Set timeout in seconds before a new image is displayed")
arg("seconds", default = "300")
run:
let c = newCommand(cTimeout, opts.seconds)
sendCommand(opts.parentOpts.server, opts.parentOpts.port, c)
command($cMode):
help(modeHelp)
arg("mode")
run:
switchMode(opts.parentOpts.server, opts.parentOpts.port, opts.mode)
try:
p.run(commandLineParams())
except:
echo p.help

157
src/providers.nim Normal file
View File

@@ -0,0 +1,157 @@
import os, sets, random, httpClient, json, strformat, options
import gintro/[gdkpixbuf, gobject]
import common
const
supportedExts = @[".png", ".jpg", ".jpeg"]
foxesUrl = "https://randomfox.ca/floof/"
inspiroUrl = "http://inspirobot.me/api?generate=true"
tmpFile = "/tmp/randopix_tmp.png"
type
FileOpResult* = object of OpResult
file*: string
ImageProvider* = ref object of RootObj
## Manages images that should be displayed
verbose: bool ## Additional logging for the image provider
mode* : Mode ## Selects the API that is used to get images
path*: Option[string] ## Path on the local file syetem that will be used in `file` mode
exts: HashSet[string] ## Allowed extensions that the `file` mode will display
files: seq[string] ## Currently loaded list of images in `file` mode
var
client = newHttpClient() ## For loading images from the web
########################
# Constructors
########################
proc newImageProvider(verbose: bool, mode: Mode, path: Option[string]): ImageProvider =
randomize()
ImageProvider(verbose: verbose, mode: mode, path: path, exts: supportedExts.toHashSet)
proc newImageProvider*(verbose: bool): ImageProvider =
newImageProvider(verbose, Mode.None, none(string))
proc newImageProvider*(verbose: bool, path: string): ImageProvider =
newImageProvider(verbose, Mode.None, some(path))
proc newImageProvider*(verbose: bool, mode: Mode): ImageProvider =
newImageProvider(verbose, mode, none(string))
proc newImageProvider*(verbose: bool, mode: Mode, path: string): ImageProvider =
newImageProvider(verbose, mode, some(path))
proc newFileOpResultError(msg: string): FileOpResult =
FileOpResult(success: false, errorMsg: msg)
proc newFileOpResult(file: string): FileOpResult =
FileOpResult(success: true, file: file)
########################
# Utilities
########################
proc log(ip: ImageProvider, msg: string) =
if ip.verbose: echo msg
########################
# Image Provider procs
########################
proc getFox(ip: ImageProvider): FileOpResult =
## Download image from the fox API
try:
let urlData = client.getContent(foxesUrl)
let info = parseJson(urlData)
let imageData = client.getContent(info["image"].getStr)
let dlFile = fmt"{tmpFile}.download"
writeFile(dlFile, imageData)
return newFileOpResult(dlFile)
except JsonParsingError:
ip.log fmt"Error while fetching from fox API: {getCurrentExceptionMsg()}"
return newFileOpResultError("Json parsing error")
except KeyError:
ip.log fmt"No image in downloaded data: {getCurrentExceptionMsg()}"
return newFileOpResultError("No image from API")
proc getInspiro(ip: ImageProvider): FileOpResult =
## Download and save image from the inspiro API
try:
let imageUrl = client.getContent(inspiroUrl)
ip.log fmt"Downloading inspiro image from: '{imageUrl}'"
let imageData = client.getContent(imageUrl)
let dlFile = fmt"{tmpFile}.download"
writeFile(dlFile,imageData)
return newFileOpResult(dlFile)
except:
ip.log fmt"Unexpected error while downloading: {getCurrentExceptionMsg()}"
return newFileOpResultError(getCurrentExceptionMsg())
proc getLocalFile(ip: var ImageProvider): FileOpResult =
## Provide an image from a local folder
# First, check if there are still images left to be loaded.
# If not reread all files from the path
if ip.files.len < 1:
if ip.path.isNone:
return newFileOpResultError("No path for image loading")
ip.log "Reloading file list..."
for file in walkDirRec(ip.path.get):
let split = splitFile(file)
if ip.exts.contains(split.ext):
ip.files.add(file)
ip.log fmt"Loaded {ip.files.len} files"
shuffle(ip.files)
# Remove the current file after
result = newFileOpResult(ip.files[0])
ip.files.delete(0)
proc getFileName(ip: var ImageProvider): FileOpResult =
## Get the temporary file name of the next file to display
case ip.mode
of Mode.File:
return ip.getLocalFile()
of Mode.Foxes:
return ip.getFox()
of Mode.Inspiro:
return ip.getInspiro()
else:
return newFileOpResultError("Not implemented")
########################
# Exported procs
########################
proc next*(ip: var ImageProvider, width, height: int): FileOpResult =
## Uses the image provider to get a new image ready to display.
## `width` and `height` should be the size of the window.
if ip.mode == Mode.None:
return newFileOpResultError("No mode active")
let op = ip.getFileName()
if not op.success: return op
var rawPixbuf = newPixbufFromFile(op.file)
# resize the pixbuf to best fit on screen
var w, h: int
if (width > height):
h = height
w = ((rawPixbuf.width * h) / rawPixbuf.height).toInt
else:
w = width
h = ((rawPixbuf.height * w) / rawPixbuf.width).toInt
var pixbuf = rawPixbuf.scaleSimple(w, h, InterpType.bilinear)
# The pixbuf is written to disk and loaded again once because
# directly setting the image from a pixbuf will leak memory
let saved = pixbuf.savev(tmpFile, "png", @[])
if not saved:
return newFileOpResultError("Error while saving temporary image")
# GTK pixbuf leaks memory when not manually decreasing reference count
pixbuf.genericGObjectUnref()
rawPixbuf.genericGObjectUnref()
newFileOpResult(tmpFile)

View File

@@ -1,5 +0,0 @@
import ../randopics
import gintro/[gdkpixbuf]
proc get(): Pixbuf =
discard

View File

@@ -1,104 +0,0 @@
import os, httpClient, json, threadpool
import gintro/[gtk, glib, gobject, gio, gdkpixbuf]
var
window: ApplicationWindow
fullscreen = true
const
floofUrl = "https://randomfox.ca/floof/"
updateTime = 300
type ImageProvider =
tuple[
get: proc(): Pixbuf
]
proc downloadImage(): Pixbuf =
let client = newHttpClient()
let urlData = client.getContent(floofUrl)
let info = parseJson(urlData)
let imageData = client.getContent(info["image"].getStr)
let loader = newPixbufLoader()
discard loader.write(imageData)
loader.getPixbuf()
proc resizeImage(pixbuf: Pixbuf, maxWidth, maxHeight: int): Pixbuf =
var width, height: int
if (maxWidth > maxHeight):
height = maxHeight
width = ((pixbuf.width * height) / pixbuf.height).toInt
else:
width = maxWidth
height = ((pixbuf.height * width) / pixbuf.width).toInt
pixbuf.scaleSimple(width, height, InterpType.bilinear)
proc replaceImage(widget: Image, width, height: int) =
var pixbuf = downloadImage()
pixbuf = pixbuf.resizeImage(width, height)
widget.setFromPixbuf(pixbuf)
proc updateCommand(action: SimpleAction; parameter: Variant; widget: Image) =
var width, height: int
window.getSize(width, height)
replaceImage(widget, width, height)
proc toggleFullscreen(action: SimpleAction; parameter: Variant; window: ApplicationWindow) =
if fullscreen:
window.unfullscreen
else:
window.fullscreen
fullscreen = not fullscreen
proc quit(action: SimpleAction; parameter: Variant; app: Application) =
app.quit()
proc runUpdater(window: ApplicationWindow, image: Image) =
echo "Start Updater"
var width, height: int
window.getSize(width, height)
spawn replaceImage(image, width, height)
proc appActivate(app: Application) =
window = newApplicationWindow(app)
window.title = "Randopics"
window.setKeepAbove(true)
let cssProvider = newCssProvider()
let data = "window { background: black; }"
discard cssProvider.loadFromData(data)
let styleContext = window.getStyleContext()
styleContext.addProvider(cssProvider, STYLE_PROVIDER_PRIORITY_USER)
let imageWidget = newImage()
window.add(imageWidget)
window.connect("show", runUpdater, imageWidget)
if fullscreen:
window.fullscreen
let fullscreenAction = newSimpleAction("fullscreen")
discard fullscreenAction.connect("activate", toggleFullscreen, window)
app.setAccelsForAction("win.fullscreen", "F")
window.actionMap.addAction(fullscreenAction)
let quitAction = newSimpleAction("quit")
discard quitAction.connect("activate", quit, app)
app.setAccelsForAction("win.quit", "Escape")
window.actionMap.addAction(quitAction)
let updateAction = newSimpleAction("update")
discard updateAction.connect("activate", updateCommand, imageWidget)
app.setAccelsForAction("win.update", "U")
window.actionMap.addAction(updateAction)
window.showAll
proc main =
let app = newApplication("org.gtk.example")
connect(app, "activate", appActivate)
discard run(app)
when isMainModule:
main()

View File

@@ -1,2 +0,0 @@
threads:on
d:ssl

246
src/randopix.nim Normal file
View File

@@ -0,0 +1,246 @@
import os, options, strformat
import gintro/[glib, gobject, gtk, gio]
import gintro/gdk except Window
import argparse except run
import providers, server, common
const
css = slurp("app.css")
version = "0.1"
type
Args = ref object
fullscreen: bool ## Applicaion is show in fullscreen mode
verbose: bool ## More debug information in notification label
timeout: int ## Milliseconds between image refreshes
var
imageProvider: ImageProvider ## Gets images from the chosen source
args: Args ## The parsed command line args
updateTimeout: int ## ID of the timeout that updates the images
# Widgets
window: ApplicationWindow
label: Label
# Server vor recieving commands from external tools
serverWorker: system.Thread[ServerArgs]
proc log(things: varargs[string, `$`]) =
if args.verbose:
echo things.join()
proc notify(label: Label, message: string = "") =
## Shows the notification box in the lower left corner.
## If no message is passed, the box will be hidden
label.text = message
if (message == ""):
label.hide
else:
label.show
proc newArgs(): Option[Args] =
let p = newParser("randopix"):
help(fmt"Version {version} - Display random images from different sources")
option("-m", "--mode", help="The image source mode.", choices=enumToStrings(Mode))
option("-p", "--path", help="Path to a directory with images for the 'file' mode")
option("-t", "--timeout", help="Seconds before the image is refreshed", default="300")
flag("-w", "--windowed", help="Do not start in fullscreen mode")
flag("-v", "--verbose", help="Show more information")
try:
let opts = p.parse(commandLineParams())
# Catch the help option. Do nothing more
if opts.help:
return
# Parse the starting mode
var startMode: Mode
try:
startMode= parseEnum[Mode](opts.mode)
except ValueError:
startMode = Mode.None
# Create the image provider
if opts.path != "":
imageProvider = newImageProvider(opts.verbose, startMode, opts.path)
else:
imageProvider = newImageProvider(opts.verbose, startMode)
## Timeout is given in seconds as an argument
var timeout = 3000
try:
timeout = opts.timeout.parseInt * 1000
except ValueError:
raise newException(UsageError, fmt"Invalid timeout value: {opts.timeout}")
return some(Args(
fullscreen: not opts.windowed,
verbose: opts.verbose,
timeout: timeout))
except:
echo p.help
proc updateImage(image: Image): bool =
## Updates the UI with a new image
try:
if args.verbose: log "Refreshing..."
if imageProvider.mode == Mode.None:
log "No display mode"
label.notify "No mode selected"
return true
var wWidth, wHeight: int
window.getSize(wWidth, wHeight)
let op = imageProvider.next(wWidth, wHeight)
result = op.success
if not op.success:
label.notify op.errorMsg
return
image.setFromFile(op.file)
label.notify
except:
let
e = getCurrentException()
msg = getCurrentExceptionMsg()
log "Got exception ", repr(e), " with message ", msg
label.notify "Error while refreshing image, retrying..."
return false
proc timedUpdate(image: Image): bool =
discard updateImage(image);
updateTimeout = int(timeoutAdd(uint32(args.timeout), timedUpdate, image))
return false
proc forceUpdate(action: SimpleAction; parameter: Variant; image: Image): void =
log "Refreshing image..."
label.notify "Refreshing image..."
if updateTimeout > 0:
discard updateTimeout.remove
updateTimeout = int(timeoutAdd(500, timedUpdate, image))
proc checkServerChannel(image: Image): bool =
## Check the channel from the control server for incomming commands
let tried = chan.tryRecv()
if tried.dataAvailable:
let msg: CommandMessage = tried.msg
log "Recieved command: ", msg.command
case msg.command
of cRefresh:
forceUpdate(nil, nil, image)
of cTimeout:
let val = msg.parameter.parseInt * 1000
log "Setting timeout to ", val
args.timeout = val
discard updateTimeout.remove
updateTimeout = int(timeoutAdd(uint32(args.timeout), timedUpdate, image))
of cMode:
try:
let mode = parseEnum[Mode](msg.parameter)
log "Switching mode: ", mode
imageProvider.mode = mode
except ValueError:
log "Invalid mode: ", msg.parameter
else:
log "Command ignored: ", msg.command
sleep(100)
result = true
proc toggleFullscreen(action: SimpleAction; parameter: Variant; window: ApplicationWindow) =
## Fullscreen toggle event
if args.fullscreen:
window.unfullscreen
else:
window.fullscreen
args.fullscreen = not args.fullscreen
proc cleanUp(w: ApplicationWindow, app: Application) =
## Stop the control server and exit the GTK application
log "Stopping control server..."
closeServer()
serverWorker.joinThread()
chan.close()
log "Server stopped."
app.quit()
proc quit(action: SimpleAction; parameter: Variant; app: Application) =
## Application quit event
cleanUp(window, app)
proc appActivate(app: Application) =
# Parse arguments from the command line
let parsed = newArgs()
if parsed.isNone:
return
else:
args = parsed.get
window = newApplicationWindow(app)
window.title = "randopix"
window.setKeepAbove(false)
window.setDefaultSize(800, 600)
# Custom styling for e.g. the background color CSS data is in the "style.nim" module
let provider = newCssProvider()
discard provider.loadFromData(css)
addProviderForScreen(getDefaultScreen(), provider, STYLE_PROVIDER_PRIORITY_USER)
# Create all windgets we are gonna use
label = newLabel("Starting...")
label.halign = Align.`end`
label.valign = Align.`end`
let container = newOverlay()
container.addOverlay(label)
window.add(container)
let image = newImage()
container.add(image)
if args.fullscreen:
window.fullscreen
## Connect the GTK signals to the procs
let fullscreenAction = newSimpleAction("fullscreen")
discard fullscreenAction.connect("activate", toggleFullscreen, window)
app.setAccelsForAction("win.fullscreen", "F")
window.actionMap.addAction(fullscreenAction)
let quitAction = newSimpleAction("quit")
discard quitAction.connect("activate", quit, app)
app.setAccelsForAction("win.quit", "Escape")
window.actionMap.addAction(quitAction)
let updateImageAction = newSimpleAction("update")
discard updateImageAction.connect("activate", forceUpdate, image)
app.setAccelsForAction("win.update", "U")
window.actionMap.addAction(updateImageAction)
window.connect("destroy", cleanUp, app)
window.showAll
# Setting the inital image
# Fix 1 second timeout to make sure all other initialization has finished
updateTimeout = int(timeoutAdd(1000, timedUpdate, image))
## open communication channel from the control server
chan.open()
## Start the server for handling incoming commands
let serverArgs = newServerArgs(args.verbose)
createThread(serverWorker, runServer, serverArgs)
discard idleAdd(checkServerChannel, image)
when isMainModule:
let app = newApplication("org.luxick.randopix")
connect(app, "activate", appActivate)
discard run(app)

3
src/randopix.nim.cfg Normal file
View File

@@ -0,0 +1,3 @@
threads:on
d:ssl
gc:arc

60
src/server.nim Normal file
View File

@@ -0,0 +1,60 @@
import net, json, strutils
import common
type
ServerArgs* = object of RootObj
verbose: bool
var
chan*: Channel[CommandMessage]
verbose: bool
proc newServerArgs*(verbose: bool): ServerArgs =
ServerArgs(verbose: verbose)
proc log(things: varargs[string, `$`]) =
if verbose:
echo things.join()
proc closeServer*() =
## Sends a "Close" command to the server
var socket = newSocket()
socket.connect("127.0.0.1", Port(defaultPort))
let c = newCommand(cClose)
socket.send(c.wrap)
socket.close()
proc runServer*[ServerArgs](arg: ServerArgs) {.thread, nimcall.} =
verbose = arg.verbose
var server = net.newSocket()
server.bindAddr(Port(defaultPort))
server.listen()
log "Control server is listening"
while true:
# Process client requests
var client = net.newSocket()
server.accept(client)
log "Client connected"
try:
var line = client.recvLine()
if line == "":
log "No data from client"
continue
var jsonData = parseJson(line)
let msg = jsonData.to(CommandMessage)
case msg.command
of cClose:
log "Server recieved termination command. Exiting."
break
else:
# Pass command from client to main applicaiton
chan.send(msg)
except OSError:
log "Server error: ", getCurrentExceptionMsg()
except:
log "Invalid command from client: ", getCurrentExceptionMsg()
log repr(getCurrentException())
server.close()

1
src/server.nim.cfg Normal file
View File

@@ -0,0 +1 @@
threads:on