use "bureaucracy"
use "collections"
use "cli"
use "logger"
use "net"
use "signals"
use "time"
actor Main
"""
A basic line-oriented chat server used as a learning example.
It features:
- graceful shutdown of the server (triggered by sending `SIGTERM` and `SIGINT`)
- logging which can be controlled via `PONY_LOG_LEVEL` environment variable
- requires clients to provide their names
- clients can send:
- `/quit` - terminate the client's connection to the server and announces
that this client has left
- `/time` - the server sends the current time to the server
- everything else is sent to all connected clients
"""
new create(env: Env) =>
let envMap = EnvVars(env.vars where prefix = "PONY_", squash = true)
let logLevel = match envMap.get_or_else("log_level", "").lower()
| "fine" => Fine
| "info" => Info
| "warn" => Warn
| "error" => Error
else
Warn
end
let custodian = Custodian
let logger = StringLogger(logLevel, env.out)
let room = ChatRoom
SignalHandler(TermHandler(custodian, logger), Sig.term())
SignalHandler(TermHandler(custodian, logger), Sig.int())
try
let server = TCPListener(
env.root as AmbientAuth,
recover ChatServer(logger, room) end,
"localhost",
"8989"
)
logger(Info) and logger.log("server started")
custodian(room)
custodian(server)
else
logger(Error) and logger.log("failed to start server")
end
class TermHandler is SignalNotify
let _custodian: Custodian
let _logger: Logger[String]
new iso create(custodian: Custodian, logger: Logger[String]) =>
_custodian = custodian
_logger = logger
fun ref apply(count: U32): Bool =>
_logger(Info) and _logger.log("going now, bye!")
_custodian.dispose()
// don't keep listening for signal
false
primitive PrintTime
fun apply(): String =>
"""Returns the current time in UTC. e.g. `2019-03-04 19:26:14`"""
(let sec: I64, let nsec: I64) = Time.now()
try
PosixDate(sec, nsec).format("%F %T %Z")?
else
"failed to format time"
end
primitive AddrStr
fun apply(n: NetAddress val): String =>
"""Returns the current `host:port` if available, `:` otherwise."""
(let host, let port) =
try n.name()?
else ("", "")
end
host + ":" + port
type Nick is String
class ChatServer is TCPListenNotify
let _logger: Logger[String]
let _room: ChatRoom
new create(logger: Logger[String], room: ChatRoom) =>
_logger = logger
_room = room
fun ref listening(listen: TCPListener ref): None val =>
_logger(Info) and _logger.log("listening on " + AddrStr(listen.local_address()))
None
fun ref not_listening(listen: TCPListener ref): None val =>
_logger(Error) and _logger.log("failed to listen on " + AddrStr(listen.local_address()))
None
fun ref connected(listen: TCPListener ref): TCPConnectionNotify iso^ =>
// create a new `ChatConnection` that handles all I/O from the connecting client
recover ChatConnection(_logger, _room) end
fun ref closed(listen: TCPListener ref): None val =>
None
class ChatConnection is TCPConnectionNotify
"""
ChatConnection handles all I/O from a client. It manages state for the connecting client.
"""
let _logger: Logger[String]
let _room: ChatRoom
var _nick: (Nick | None)
new create(logger: Logger[String], room: ChatRoom) =>
_logger = logger
_room = room
_nick = None
fun ref accepted(conn: TCPConnection ref) : None val =>
_logger(Info) and _logger.log("new client conection accepted from " + AddrStr(conn.remote_address()))
conn.write("welcome! Please enter your name: \n")
None
fun ref connect_failed(conn: TCPConnection ref): None val =>
// Hmmm? This seems like it gets called if we were a client and not a server
None
fun ref received(conn: TCPConnection ref, data: Array[U8] iso, times: USize): Bool =>
_logger(Info) and _logger.log("received data from " + AddrStr(conn.remote_address()))
// state management for client...not great but it's a start and good enough for an example
match _nick
| None =>
match String.from_iso_array(consume data).>strip()
| let nick: String val if nick.size() > 0 =>
_nick = nick
_room.add(conn, nick)
else
conn.write("Please enter your name: \n")
end
| let nick: String =>
let line: String val = String.from_iso_array(consume data).>strip()
match line
| "/quit" =>
_room.remove(conn)
return false
| "/time" => conn.write(PrintTime() + "\n")
| let msg: String if msg.size() > 0 =>
_room.message(nick, msg)
end
end
true
fun ref closed(conn: TCPConnection ref): None val =>
_logger(Info) and _logger.log("client closed connection")
match _nick
| let _: String => _room.remove(conn)
end
None
actor ChatRoom
let _conns: MapIs[TCPConnection, Nick] = _conns.create()
be dispose() =>
message("server", "shutting down...")
_shutdown()
be _shutdown() =>
for conn in _conns.keys() do
conn.mute()
conn.dispose()
end
_conns.clear()
be add(conn: TCPConnection, nick: Nick) =>
_conns(conn) = nick
be remove(conn: TCPConnection) =>
try
let nick = _conns(conn)?
_conns.remove(conn)?
conn.mute()
conn.dispose()
message("server", nick + " left")
end
be message(nick: Nick, msg: String) =>
for (conn, nick') in _conns.pairs() do
if nick != nick' then
conn.write(nick + ": " + msg + "\n")
end
end
// object literal
class Foo
fun foo(str: String): Hashable iso^ =>
object iso is Hashable
fun apply(): String => str
fun hash(): U64 => str.hash()
end
// lambda
class Foo2
new create(env:Env) =>
foo({(s: String)(env) => env.out.print(s) })
fun foo(f: {(String)}) =>
f("Hello World")