Skip to content
Merged
23 changes: 16 additions & 7 deletions apps/core/lib/prodigy/core/data/session.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,33 @@

defmodule Prodigy.Core.Data.Session do
use Ecto.Schema
import Ecto.Changeset

@moduledoc """
Schema specific to individual user sessions and related change functions
"""

schema "session" do
belongs_to(:user, Prodigy.Core.Data.User)
belongs_to(:user, Prodigy.Core.Data.User, type: :string)
field(:logon_timestamp, :utc_datetime)
field(:logon_status, :integer) # enroll, success, etc; this can be a small integer or enum
field(:logon_status, :integer) # 0=success, 1=enroll_other, 2=enroll_subscriber
field(:logoff_timestamp, :utc_datetime)
field(:logoff_status, :integer) # normal, abnormal, bounced, etc; this can be a small integer or enum
field(:logoff_status, :integer) # 0=normal, 1=abnormal, 2=timeout, 3=forced
field(:rs_version, :string)
field(:node, :string) # for forced disconnect and clearing stale sessions
field(:node, :string)
field(:pid, :string)

# TODO need a special mechanism to get origin IP if session originates from a softmodem that answers a SIP call
# use the native inet field if possible
field(:source_address, :string)
field(:source_port, :integer)
field(:last_activity_at, :utc_datetime)

timestamps()
end

def changeset(session, attrs) do
session
|> cast(attrs, [:user_id, :logon_timestamp, :logon_status, :logoff_timestamp,
:logoff_status, :rs_version, :node, :pid, :source_address,
:source_port, :last_activity_at])
|> validate_required([:user_id, :logon_timestamp, :logon_status, :node, :pid])
end
end
3 changes: 2 additions & 1 deletion apps/core/lib/prodigy/core/data/user.ex
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ defmodule Prodigy.Core.Data.User do
field(:gender, :string)
field(:date_enrolled, :date)
field(:date_deleted, :date)
field(:logged_on, :boolean)
has_many(:sessions, Prodigy.Core.Data.Session)
field(:concurrency_limit, :integer, default: 1)
field(:last_name, :string)
field(:first_name, :string)
field(:middle_name, :string)
Expand Down
35 changes: 35 additions & 0 deletions apps/core/priv/repo/migrations/20250908221355_create_sessions.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
defmodule Prodigy.Core.Data.Repo.Migrations.CreateSessions do
use Ecto.Migration

def change do
create table(:session) do
add :user_id, references(:user, type: :string, on_delete: :nothing), null: false
add :logon_timestamp, :utc_datetime, null: false
add :logon_status, :integer, null: false # 0=success, 1=enroll_other, 2=enroll_subscriber
add :logoff_timestamp, :utc_datetime
add :logoff_status, :integer # 0=normal, 1=abnormal, 2=timeout, 3=forced
add :rs_version, :string
add :node, :string, null: false
add :pid, :string, null: false
add :source_address, :string
add :source_port, :integer
add :last_activity_at, :utc_datetime

timestamps()
end

create index(:session, [:user_id])
create index(:session, [:logoff_timestamp])
create index(:session, [:node])

# Add concurrency_limit to users
alter table(:user) do
add :concurrency_limit, :integer, default: 1
end

# Remove logged_on from users (or keep it temporarily for rollback safety)
alter table(:user) do
remove :logged_on
end
end
end
15 changes: 11 additions & 4 deletions apps/pomsutil/lib/prodigy/oms_util/cli.ex
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ defmodule Prodigy.OmsUtil.CLI do

alias Prodigy.Core.Data.Util

@spec usage(any()) :: none()
def usage(mode, message \\ "")

def usage(:terse, message) do
Expand Down Expand Up @@ -67,13 +68,16 @@ defmodule Prodigy.OmsUtil.CLI do
list-users [--like <pattern>]
Displays a listing of user accounts found at <datasource>

create [XXXX[YY]] [password]
create [XXXX[YY]] [password] [--concurrency-limit <n>]
When no arguments, XXXX, or XXXXYY are given, creates a household
with household ID next in sequence, or with XXXXYY as specified.

Creates the "A" user record, with the given password, or if none is
given, generates one randomly which is printed to the console.

--concurrency-limit <n> Set the maximum concurrent sessions for this user
(default: 1, use 0 for unlimited)

If XXXXYY already exists, an error is given.

delete <XXXXYY[Z]>
Expand All @@ -99,15 +103,17 @@ defmodule Prodigy.OmsUtil.CLI do
d: :database,
p: :port,
h: :host,
u: :username
u: :username,
c: :concurrency_limit
],
strict: [
help: :boolean,
user: :string,
host: :string,
port: :integer,
database: :string,
like: :string
like: :string,
concurrency_limit: :integer
]
)

Expand Down Expand Up @@ -141,7 +147,8 @@ defmodule Prodigy.OmsUtil.CLI do
end

args = %{
id: id
id: id,
concurrency_limit: Map.get(args, :concurrency_limit, 1)
}

Create.exec(rest, args)
Expand Down
23 changes: 21 additions & 2 deletions apps/pomsutil/lib/prodigy/oms_util/create.ex
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,15 @@ defmodule Create do

import Ecto.Changeset

def exec(argv, _args \\ %{}) do
def exec(argv, args \\ %{}) do
{household_id, password} = parse_arguments(argv)
concurrency_limit = Map.get(args, :concurrency_limit, 1)

# Validate concurrency limit
if concurrency_limit < 0 do
IO.puts("Error: Concurrency limit must be 0 (unlimited) or a positive integer")
exit({:shutdown, 1})
end

today = DateTime.to_date(DateTime.utc_now())

Expand All @@ -30,14 +37,26 @@ defmodule Create do
%Household{id: household_id, enabled_date: today}
|> change
|> put_assoc(:users, [
%User{id: household_id <> "A"}
%User{
id: household_id <> "A",
concurrency_limit: concurrency_limit # Set the limit
}
|> User.changeset(%{password: password})
])
|> Repo.insert()

IO.puts("- Created Household #{household_id}")
IO.puts("- Created User #{household_id <> "A"} with password #{password}")

# Show concurrency limit info
limit_msg = if concurrency_limit == 0 do
"unlimited concurrent sessions"
else
"#{concurrency_limit} concurrent session(s)"
end
IO.puts(" * Concurrency limit: #{limit_msg}")


_existing ->
IO.puts("Error: Household #{household_id} already exists")
exit({:shutdown, 1})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@
# You should have received a copy of the GNU Affero General Public License along with Prodigy Reloaded. If not,
# see <https://www.gnu.org/licenses/>.

defmodule Prodigy.Server.Session do
defmodule Prodigy.Server.Context do
@moduledoc """
Structure containing information for an individual Prodigy Session.
Structure containing context for an individual Prodigy Connection.

The Session structure is established when the `Prodigy.Server.Router` instance is created upon client connection, and
The Context structure is established when the `Prodigy.Server.Router` instance is created upon client connection, and
persists until the connection is terminated.
"""

Expand Down
7 changes: 6 additions & 1 deletion apps/server/lib/prodigy/server/prodigy.ex
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,17 @@ defmodule Prodigy.Server.Application do
def start(_type, _args) do
Logger.info("Starting Prodigy Server")

# these processes are started in this order, then shutdown in reverse order when the process receives SIGKILL
# SessionCleanup will close any active sessions on this node. Good for routine shutdowns, but would also be good
# if nodes had deterministic names and could clean stale sessions on restart.
children = [
{Prodigy.Server.RanchSup, {}},
{Prodigy.Core.Data.Repo, []},
Prodigy.Server.SessionCleanup,
{Prodigy.Server.RanchSup, {}},
Prodigy.Server.Scheduler
]

Supervisor.start_link(children, strategy: :one_for_one)
end

end
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ defmodule Prodigy.Server.Protocol.Dia do
end

@impl GenServer
def terminate(reason, state) do
def terminate(reason, _state) do
Logger.debug("DIA server shutting down: #{inspect(reason)}")
# Process.exit(state.router_pid, :shutdown)
:normal
Expand Down
4 changes: 2 additions & 2 deletions apps/server/lib/prodigy/server/protocol/dia/packet.ex
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ defmodule Prodigy.Server.Protocol.Dia.Packet do

payload = fm4 <> fm9 <> fm64 <> packet.payload

<<16, bool2int(packet.concatenated)::1, 0::7, packet.function.value,
<<16, bool2int(packet.concatenated)::1, 0::7, packet.function.value(),
Fm0.Mode.encode(packet.mode)::binary, packet.src::32, packet.logon_seq, packet.message_id,
packet.dest::32, byte_size(payload)::16, payload::binary>>
end
Expand All @@ -152,7 +152,7 @@ defmodule Prodigy.Server.Protocol.Dia.Packet do

@spec encode(Fm64.t()) :: binary()
def encode(%Fm64{} = packet) do
<<6, 0::1, 64::7, packet.status_type.value, packet.data_mode.value,
<<6, 0::1, 64::7, packet.status_type.value(), packet.data_mode.value(),
byte_size(packet.payload)::16, packet.payload::binary>>
end

Expand Down
2 changes: 1 addition & 1 deletion apps/server/lib/prodigy/server/protocol/tcs/tcs_packet.ex
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ defmodule Prodigy.Server.Protocol.Tcs.Packet do
@spec encode(Packet.t()) :: <<_::16, _::_*8>>
def encode(%Packet{} = packet) do
count = byte_size(packet.payload) - 1
data = <<count, ~~~count &&& 255, packet.seq, packet.type.value, packet.payload::binary>>
data = <<count, ~~~count &&& 255, packet.seq, packet.type.value(), packet.payload::binary>>
crc = CRC.calculate(:binary.bin_to_list(data), :x_25)
<<0x02>> <> data <> <<crc::16-little>>
end
Expand Down
11 changes: 6 additions & 5 deletions apps/server/lib/prodigy/server/protocol/tcs/tcs_protocol.ex
Original file line number Diff line number Diff line change
Expand Up @@ -208,13 +208,14 @@ defmodule Prodigy.Server.Protocol.Tcs do
end

@impl GenServer
def terminate(reason, state) do
Logger.debug("TCS server shutting down: #{inspect(reason)}")
:normal
def handle_info({:EXIT, _pid, _reason}, state) do
{:stop, :normal, state}
end

@impl GenServer
def handle_info({:EXIT, _pid, _reason}, state) do
{:stop, :normal, state}
def terminate(reason, _state) do
Logger.debug("TCS server shutting down: #{inspect(reason)}")
:normal
end

end
44 changes: 22 additions & 22 deletions apps/server/lib/prodigy/server/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,23 @@

defmodule Prodigy.Server.Router do
@moduledoc """
The Router implements the service invocation function and stores session data.
The Router implements the service invocation function and stores context data.

An instance of the Router is created for each active connection.

The Router is responsible for:
* Storing an instance of `Prodigy.Server.Session`
* Storing an instance of `Prodigy.Server.Context`
* receiving packets from `Prodigy.Server.Protocol.Dia`
* invoking the appropriate Service (that implements `Prodigy.Server.Service`) and passing to it the packet itself and
the session
* Storing the updated Session as received from the Service
the context
* Storing the updated Context as received from the Service
* Returning any response received from the service to the reception system (note, DIA vs TOCS)
"""

require Logger
use GenServer
alias Prodigy.Server.Protocol.Dia.Packet.Fm0
alias Prodigy.Server.Session
alias Prodigy.Server.Context

alias Prodigy.Server.Service.{
AddressBook,
Expand All @@ -48,7 +48,7 @@ defmodule Prodigy.Server.Router do
}

defmodule State do
defstruct session: %Session{}
defstruct context: %Context{}
end

def handle_packet(pid, %Fm0{} = packet), do: GenServer.call(pid, {:handle_packet, packet})
Expand All @@ -57,7 +57,7 @@ defmodule Prodigy.Server.Router do
def init(_) do
Logger.debug("router started")
Process.flag(:trap_exit, true)
{:ok, %State{session: %Session{auth_timeout: Session.set_auth_timer()}}}
{:ok, %State{context: %Context{auth_timeout: Context.set_auth_timer()}}}
end

defmodule Default do
Expand All @@ -78,13 +78,13 @@ defmodule Prodigy.Server.Router do
* DIA destination ID
* When necessary, the first byte of the DIA packet payload

The entire deserialized DIA Fm0 packet (`Prodigy.Server.Protocol.Dia.Packet.Fm0`) and the `Prodigy.Server.Session` is
The entire deserialized DIA Fm0 packet (`Prodigy.Server.Protocol.Dia.Packet.Fm0`) and the `Prodigy.Server.Context` is
passed to the service. The service returns:
* A status atom (:ok, :error, or :disconnect)
* The `Prodigy.Server.Session` struct, updated as appropriate
* The `Prodigy.Server.Context` struct, updated as appropriate
* Optionally, A binary response payload

The router will update the stored `Prodigy.Server.Session` with the value returned, and the binary response payload
The router will update the stored `Prodigy.Server.Context` with the value returned, and the binary response payload
will be returned to `Prodigy.Server.Protocol.Dia`, then to `Prodigy.Server.Protocol.Tcs` where it will ultimately be
chunked, encapsulated, and sent to the Reception System.
"""
Expand Down Expand Up @@ -138,25 +138,25 @@ defmodule Prodigy.Server.Router do
Default
end

case service.handle(packet, state.session) do
{:ok, %Session{} = session} ->
{:reply, {:ok}, %{state | session: session}}
case service.handle(packet, state.context) do
{:ok, %Context{} = context} ->
{:reply, {:ok}, %{state | context: context}}

{:ok, %Session{} = session, response} ->
{:reply, {:ok, response}, %{state | session: session}}
{:ok, %Context{} = context, response} ->
{:reply, {:ok, response}, %{state | context: context}}

{:error, %Session{} = session, response} ->
{:reply, {:ok, response}, %{state | session: session}}
{:error, %Context{} = context, response} ->
{:reply, {:ok, response}, %{state | context: context}}

# but want to exit at the end of this
{:disconnect, %Session{}, response} ->
{:reply, {:ok, response}, %Session{}}
{:disconnect, %Context{}, response} ->
{:reply, {:ok, response}, %Context{}}
end
end

@impl GenServer
def terminate(reason, %{session: %Session{user: user}} = _state) do
# If the router is terminated with a session still active, log the user off
def terminate(reason, %{context: %Context{user: user}} = _state) do
# If the router is terminated with a connection still active, log the user off
Logoff.handle_abnormal(user)
Logger.debug("Router shutting down: #{inspect(reason)}")
:normal
Expand All @@ -171,7 +171,7 @@ defmodule Prodigy.Server.Router do

@impl true
def handle_info(:auth_timeout, state) do
Logger.warn("authentication timeout")
Logger.warning("authentication timeout")
{:stop, :normal, state}
end

Expand Down
Loading