This commit is contained in:
2025-08-25 06:28:42 +02:00
parent 5f683ec4a8
commit 836c87fbec
44 changed files with 891 additions and 942 deletions

View File

@@ -25,19 +25,19 @@ fn test_user_mgmt() ! {
mut lf := linux.new()! mut lf := linux.new()!
// Test user creation // Test user creation
lf.user_create( lf.user_create(
name: 'testuser' name: 'testuser'
sshkey: 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIM3/2K7R8A/l0kM0/d' sshkey: 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIM3/2K7R8A/l0kM0/d'
)! )!
// Test ssh key creation // Test ssh key creation
lf.sshkey_create( lf.sshkey_create(
username: 'testuser' username: 'testuser'
sshkey_name: 'testkey' sshkey_name: 'testkey'
)! )!
// Test ssh key deletion // Test ssh key deletion
lf.sshkey_delete( lf.sshkey_delete(
username: 'testuser' username: 'testuser'
sshkey_name: 'testkey' sshkey_name: 'testkey'
)! )!

View File

@@ -1,6 +1,5 @@
#!/usr/bin/env -S v -n -w -gc none -cc tcc -d use_openssl -enable-globals run #!/usr/bin/env -S v -n -w -gc none -cc tcc -d use_openssl -enable-globals run
import freeflowuniverse.herolib.osal.tmux import freeflowuniverse.herolib.osal.tmux
mut t := tmux.new()! mut t := tmux.new()!

View File

@@ -6,7 +6,7 @@ import time
mut t := tmux.new()! mut t := tmux.new()!
if !t.is_running()! { if !t.is_running()! {
t.start()! t.start()!
} }
// Create a session and window // Create a session and window
@@ -18,15 +18,15 @@ time.sleep(1000 * time.millisecond)
// Get the active pane // Get the active pane
if mut pane := window.pane_active() { if mut pane := window.pane_active() {
// Get process info for the pane and its children // Get process info for the pane and its children
process_map := pane.processinfo()! process_map := pane.processinfo()!
println('Process tree for pane ${pane.id}:') println('Process tree for pane ${pane.id}:')
for process in process_map.processes { for process in process_map.processes {
println(' PID: ${process.pid}, CPU: ${process.cpu_perc}%, Memory: ${process.mem_perc}%, Command: ${process.cmd}') println(' PID: ${process.pid}, CPU: ${process.cpu_perc}%, Memory: ${process.mem_perc}%, Command: ${process.cmd}')
} }
// Get just the main process info // Get just the main process info
main_process := pane.processinfo_main()! main_process := pane.processinfo_main()!
println('\nMain process: PID ${main_process.pid}, Command: ${main_process.cmd}') println('\nMain process: PID ${main_process.pid}, Command: ${main_process.cmd}')
} }

View File

@@ -7,13 +7,13 @@ import time
fn (mut c LivekitClient) post(path string, body any) !http.Response { fn (mut c LivekitClient) post(path string, body any) !http.Response {
mut token := c.new_access_token( mut token := c.new_access_token(
identity: 'api' identity: 'api'
name: 'API User' name: 'API User'
ttl: 10 * 60 // 10 minutes ttl: 10 * 60 // 10 minutes
)! )!
token.add_video_grant(VideoGrant{ token.add_video_grant(VideoGrant{
room_create: true room_create: true
room_admin: true room_admin: true
room_list: true room_list: true
}) })
jwt := token.to_jwt()! jwt := token.to_jwt()!
@@ -25,9 +25,9 @@ fn (mut c LivekitClient) post(path string, body any) !http.Response {
data := json.encode(body) data := json.encode(body)
mut req := http.Request{ mut req := http.Request{
method: .post method: .post
url: url url: url
header: header header: header
data: data data: data
} }
resp := http.fetch(req)! resp := http.fetch(req)!
if resp.status_code != 200 { if resp.status_code != 200 {

View File

@@ -3,13 +3,16 @@ module livekit
import freeflowuniverse.herolib.data.caching import freeflowuniverse.herolib.data.caching
import os import os
const CACHING_METHOD = caching.CachingMethod.once_per_process // const CACHING_METHOD = caching.CachingMethod.once_per_process
fn _init() ! { fn _init() ! {
if caching.is_set(key: 'livekit_clients') { if caching.is_set(key: 'livekit_clients') {
return return
} }
caching.set[map[string]LivekitClient](key: 'livekit_clients', val: map[string]LivekitClient{}, CachingMethod.once_per_process)! caching.set[map[string]LivekitClient](
key: 'livekit_clients'
val: map[string]LivekitClient{}
)!
} }
fn _get() !map[string]LivekitClient { fn _get() !map[string]LivekitClient {
@@ -25,7 +28,7 @@ pub fn get(name string) !LivekitClient {
pub fn set(client LivekitClient) ! { pub fn set(client LivekitClient) ! {
mut clients := _get()! mut clients := _get()!
clients[client.name] = client clients[client.name] = client
caching.set[map[string]LivekitClient](key: 'livekit_clients', val: clients, CachingMethod.once_per_process)! caching.set[map[string]LivekitClient](key: 'livekit_clients', val: clients)!
} }
pub fn exists(name string) !bool { pub fn exists(name string) !bool {

View File

@@ -2,9 +2,9 @@ module livekit
pub struct SendDataArgs { pub struct SendDataArgs {
pub mut: pub mut:
room_name string room_name string
data []u8 data []u8
kind DataPacket_Kind kind DataPacket_Kind
destination_sids []string destination_sids []string
} }

View File

@@ -4,40 +4,40 @@ import json
pub struct EgressInfo { pub struct EgressInfo {
pub mut: pub mut:
egress_id string egress_id string
room_id string room_id string
status string status string
started_at i64 started_at i64
ended_at i64 ended_at i64
error string error string
} }
pub struct StartRoomCompositeEgressArgs { pub struct StartRoomCompositeEgressArgs {
pub mut: pub mut:
room_name string room_name string
layout string layout string
audio_only bool audio_only bool
video_only bool video_only bool
custom_base_url string custom_base_url string
} }
pub struct StartTrackCompositeEgressArgs { pub struct StartTrackCompositeEgressArgs {
pub mut: pub mut:
room_name string room_name string
audio_track_id string audio_track_id string
video_track_id string video_track_id string
} }
pub struct StartWebEgressArgs { pub struct StartWebEgressArgs {
pub mut: pub mut:
url string url string
audio_only bool audio_only bool
video_only bool video_only bool
} }
pub struct UpdateStreamArgs { pub struct UpdateStreamArgs {
pub mut: pub mut:
add_output_urls []string add_output_urls []string
remove_output_urls []string remove_output_urls []string
} }
@@ -60,25 +60,36 @@ pub fn (mut c LivekitClient) start_web_egress(args StartWebEgressArgs) !EgressIn
} }
pub fn (mut c LivekitClient) update_layout(egress_id string, layout string) !EgressInfo { pub fn (mut c LivekitClient) update_layout(egress_id string, layout string) !EgressInfo {
mut resp := c.post('twirp/livekit.Egress/UpdateLayout', {'egress_id': egress_id, 'layout': layout})! mut resp := c.post('twirp/livekit.Egress/UpdateLayout', {
'egress_id': egress_id
'layout': layout
})!
egress_info := json.decode[EgressInfo](resp.body)! egress_info := json.decode[EgressInfo](resp.body)!
return egress_info return egress_info
} }
pub fn (mut c LivekitClient) update_stream(egress_id string, args UpdateStreamArgs) !EgressInfo { pub fn (mut c LivekitClient) update_stream(egress_id string, args UpdateStreamArgs) !EgressInfo {
mut resp := c.post('twirp/livekit.Egress/UpdateStream', {'egress_id': egress_id, 'add_output_urls': args.add_output_urls, 'remove_output_urls': args.remove_output_urls})! mut resp := c.post('twirp/livekit.Egress/UpdateStream', {
'egress_id': egress_id
'add_output_urls': args.add_output_urls
'remove_output_urls': args.remove_output_urls
})!
egress_info := json.decode[EgressInfo](resp.body)! egress_info := json.decode[EgressInfo](resp.body)!
return egress_info return egress_info
} }
pub fn (mut c LivekitClient) list_egress(room_name string) ![]EgressInfo { pub fn (mut c LivekitClient) list_egress(room_name string) ![]EgressInfo {
mut resp := c.post('twirp/livekit.Egress/ListEgress', {'room_name': room_name})! mut resp := c.post('twirp/livekit.Egress/ListEgress', {
'room_name': room_name
})!
egress_infos := json.decode[[]EgressInfo](resp.body)! egress_infos := json.decode[[]EgressInfo](resp.body)!
return egress_infos return egress_infos
} }
pub fn (mut c LivekitClient) stop_egress(egress_id string) !EgressInfo { pub fn (mut c LivekitClient) stop_egress(egress_id string) !EgressInfo {
mut resp := c.post('twirp/livekit.Egress/StopEgress', {'egress_id': egress_id})! mut resp := c.post('twirp/livekit.Egress/StopEgress', {
'egress_id': egress_id
})!
egress_info := json.decode[EgressInfo](resp.body)! egress_info := json.decode[EgressInfo](resp.body)!
return egress_info return egress_info
} }

View File

@@ -77,30 +77,20 @@ pub mut:
pub struct InputAudioState { pub struct InputAudioState {
pub mut: pub mut:
mime_type string mime_type string
channels u32 channels u32
sample_rate u32 sample_rate u32
} }
pub struct CreateIngressArgs { pub struct CreateIngressArgs {
pub mut: pub mut:
name string name string
room_name string room_name string
participant_identity string participant_identity string
participant_name string participant_name string
input_type IngressInput input_type IngressInput
audio IngressAudioOptions audio IngressAudioOptions
video IngressVideoOptions video IngressVideoOptions
}
pub struct UpdateIngressArgs {
pub mut:
name string
room_name string
participant_identity string
participant_name string
audio IngressAudioOptions
video IngressVideoOptions
} }
pub fn (mut c LivekitClient) create_ingress(args CreateIngressArgs) !IngressInfo { pub fn (mut c LivekitClient) create_ingress(args CreateIngressArgs) !IngressInfo {
@@ -109,20 +99,43 @@ pub fn (mut c LivekitClient) create_ingress(args CreateIngressArgs) !IngressInfo
return ingress_info return ingress_info
} }
pub fn (mut c LivekitClient) update_ingress(ingress_id string, args UpdateIngressArgs) !IngressInfo { pub struct UpdateIngressArgs {
mut resp := c.post('twirp/livekit.Ingress/UpdateIngress', {'ingress_id': ingress_id, ...args})! pub mut:
ingress_id string
name string
room_name string
participant_identity string
participant_name string
audio IngressAudioOptions
video IngressVideoOptions
}
pub fn (mut c LivekitClient) update_ingress(args UpdateIngressArgs) !IngressInfo {
mut resp := c.post('twirp/livekit.Ingress/UpdateIngress', {
'ingress_id': args.ingress_id
'name': args.name
'room_name': args.room_name
'participant_identity': args.participant_identity
'participant_name': args.participant_name
'audio': args.audio
'video': args.video
})!
ingress_info := json.decode[IngressInfo](resp.body)! ingress_info := json.decode[IngressInfo](resp.body)!
return ingress_info return ingress_info
} }
pub fn (mut c LivekitClient) list_ingress(room_name string) ![]IngressInfo { pub fn (mut c LivekitClient) list_ingress(room_name string) ![]IngressInfo {
mut resp := c.post('twirp/livekit.Ingress/ListIngress', {'room_name': room_name})! mut resp := c.post('twirp/livekit.Ingress/ListIngress', {
'room_name': room_name
})!
ingress_infos := json.decode[[]IngressInfo](resp.body)! ingress_infos := json.decode[[]IngressInfo](resp.body)!
return ingress_infos return ingress_infos
} }
pub fn (mut c LivekitClient) delete_ingress(ingress_id string) !IngressInfo { pub fn (mut c LivekitClient) delete_ingress(ingress_id string) !IngressInfo {
mut resp := c.post('twirp/livekit.Ingress/DeleteIngress', {'ingress_id': ingress_id})! mut resp := c.post('twirp/livekit.Ingress/DeleteIngress', {
'ingress_id': ingress_id
})!
ingress_info := json.decode[IngressInfo](resp.body)! ingress_info := json.decode[IngressInfo](resp.body)!
return ingress_info return ingress_info
} }

View File

@@ -4,23 +4,23 @@ import json
pub struct ParticipantInfo { pub struct ParticipantInfo {
pub mut: pub mut:
sid string sid string
identity string identity string
state string state string
metadata string metadata string
joined_at i64 joined_at i64
name string name string
version u32 version u32
permission string permission string
region string region string
publisher bool publisher bool
} }
pub struct UpdateParticipantArgs { pub struct UpdateParticipantArgs {
pub mut: pub mut:
room_name string room_name string
identity string identity string
metadata string metadata string
permission string permission string
} }
@@ -33,19 +33,27 @@ pub mut:
} }
pub fn (mut c LivekitClient) list_participants(room_name string) ![]ParticipantInfo { pub fn (mut c LivekitClient) list_participants(room_name string) ![]ParticipantInfo {
mut resp := c.post('twirp/livekit.RoomService/ListParticipants', {'room': room_name})! mut resp := c.post('twirp/livekit.RoomService/ListParticipants', {
'room': room_name
})!
participants := json.decode[[]ParticipantInfo](resp.body)! participants := json.decode[[]ParticipantInfo](resp.body)!
return participants return participants
} }
pub fn (mut c LivekitClient) get_participant(room_name string, identity string) !ParticipantInfo { pub fn (mut c LivekitClient) get_participant(room_name string, identity string) !ParticipantInfo {
mut resp := c.post('twirp/livekit.RoomService/GetParticipant', {'room': room_name, 'identity': identity})! mut resp := c.post('twirp/livekit.RoomService/GetParticipant', {
'room': room_name
'identity': identity
})!
participant := json.decode[ParticipantInfo](resp.body)! participant := json.decode[ParticipantInfo](resp.body)!
return participant return participant
} }
pub fn (mut c LivekitClient) remove_participant(room_name string, identity string) ! { pub fn (mut c LivekitClient) remove_participant(room_name string, identity string) ! {
_ = c.post('twirp/livekit.RoomService/RemoveParticipant', {'room': room_name, 'identity': identity})! _ = c.post('twirp/livekit.RoomService/RemoveParticipant', {
'room': room_name
'identity': identity
})!
} }
pub fn (mut c LivekitClient) update_participant(args UpdateParticipantArgs) ! { pub fn (mut c LivekitClient) update_participant(args UpdateParticipantArgs) ! {

View File

@@ -20,9 +20,9 @@ pub fn play(mut plbook PlayBook) ! {
api_secret := p.get('api_secret')! api_secret := p.get('api_secret')!
mut client := LivekitClient{ mut client := LivekitClient{
name: name name: name
url: url url: url
api_key: api_key api_key: api_key
api_secret: api_secret api_secret: api_secret
} }
@@ -45,10 +45,10 @@ pub fn play(mut plbook PlayBook) ! {
mut client := get(name: client_name)! mut client := get(name: client_name)!
room := client.create_room( room := client.create_room(
name: room_name name: room_name
empty_timeout: empty_timeout empty_timeout: empty_timeout
max_participants: max_participants max_participants: max_participants
metadata: metadata metadata: metadata
)! )!
console.print_header('Room "${room_name}" created successfully') console.print_header('Room "${room_name}" created successfully')
@@ -100,9 +100,9 @@ pub fn play(mut plbook PlayBook) ! {
mut client := get(name: client_name)! mut client := get(name: client_name)!
client.mute_published_track( client.mute_published_track(
room_name: room_name room_name: room_name
identity: identity identity: identity
track_sid: track_sid track_sid: track_sid
muted: muted muted: muted
)! )!
status := if muted { 'muted' } else { 'unmuted' } status := if muted { 'muted' } else { 'unmuted' }
@@ -122,7 +122,7 @@ pub fn play(mut plbook PlayBook) ! {
mut client := get(name: client_name)! mut client := get(name: client_name)!
client.update_room_metadata( client.update_room_metadata(
room_name: room_name room_name: room_name
metadata: metadata metadata: metadata
)! )!
console.print_header('Room "${room_name}" metadata updated') console.print_header('Room "${room_name}" metadata updated')
@@ -147,15 +147,15 @@ pub fn play(mut plbook PlayBook) ! {
mut token := client.new_access_token( mut token := client.new_access_token(
identity: identity identity: identity
name: name name: name
ttl: ttl ttl: ttl
)! )!
token.add_video_grant(VideoGrant{ token.add_video_grant(VideoGrant{
room: room room: room
room_join: true room_join: true
can_publish: can_publish can_publish: can_publish
can_subscribe: can_subscribe can_subscribe: can_subscribe
can_publish_data: can_publish_data can_publish_data: can_publish_data
}) })

View File

@@ -5,25 +5,25 @@ import net.http
pub struct Room { pub struct Room {
pub mut: pub mut:
sid string sid string
name string name string
empty_timeout u32 empty_timeout u32
max_participants u32 max_participants u32
creation_time i64 creation_time i64
turn_password string turn_password string
enabled_codecs []string enabled_codecs []string
metadata string metadata string
num_participants u32 num_participants u32
num_connected_participants u32 num_connected_participants u32
active_recording bool active_recording bool
} }
pub struct CreateRoomArgs { pub struct CreateRoomArgs {
pub mut: pub mut:
name string name string
empty_timeout u32 empty_timeout u32
max_participants u32 max_participants u32
metadata string metadata string
} }
pub struct UpdateRoomMetadataArgs { pub struct UpdateRoomMetadataArgs {
@@ -39,7 +39,9 @@ pub fn (mut c LivekitClient) create_room(args CreateRoomArgs) !Room {
} }
pub fn (mut c LivekitClient) delete_room(room_name string) ! { pub fn (mut c LivekitClient) delete_room(room_name string) ! {
_ = c.post('twirp/livekit.RoomService/DeleteRoom', {'room': room_name})! _ = c.post('twirp/livekit.RoomService/DeleteRoom', {
'room': room_name
})!
} }
pub fn (mut c LivekitClient) update_room_metadata(args UpdateRoomMetadataArgs) ! { pub fn (mut c LivekitClient) update_room_metadata(args UpdateRoomMetadataArgs) ! {

View File

@@ -15,19 +15,19 @@ pub:
unix_micro string unix_micro string
} }
pub struct Room { // pub struct Room {
pub: // pub:
active_recording bool // active_recording bool
creation_time string // creation_time string
departure_timeout int // departure_timeout int
empty_timeout int // empty_timeout int
enabled_codecs []Codec // enabled_codecs []Codec
max_participants int // max_participants int
metadata string // metadata string
name string // name string
num_participants int // num_participants int
num_publishers int // num_publishers int
sid string // sid string
turn_password string // turn_password string
version Version // version Version
} // }

View File

@@ -5,33 +5,33 @@ import time
pub struct AccessToken { pub struct AccessToken {
pub mut: pub mut:
api_key string api_key string
api_secret string api_secret string
identity string identity string
name string name string
ttl int ttl int
video_grant VideoGrant video_grant VideoGrant
} }
pub struct VideoGrant { pub struct VideoGrant {
pub mut: pub mut:
room_create bool room_create bool
room_admin bool room_admin bool
room_join bool room_join bool
room_list bool room_list bool
can_publish bool can_publish bool
can_subscribe bool can_subscribe bool
can_publish_data bool can_publish_data bool
room string room string
} }
pub fn (mut c LivekitClient) new_access_token(identity string, name string, ttl int) !AccessToken { pub fn (mut c LivekitClient) new_access_token(identity string, name string, ttl int) !AccessToken {
return AccessToken{ return AccessToken{
api_key: c.api_key api_key: c.api_key
api_secret: c.api_secret api_secret: c.api_secret
identity: identity identity: identity
name: name name: name
ttl: ttl ttl: ttl
} }
} }

View File

@@ -18,60 +18,60 @@ pub mut:
name string name string
} }
// VideoGrant struct placeholder // // VideoGrant struct placeholder
pub struct VideoGrant { // pub struct VideoGrant {
pub mut: // pub mut:
room string // room string
room_join bool @[json: 'roomJoin'] // room_join bool @[json: 'roomJoin']
room_list bool @[json: 'roomList'] // room_list bool @[json: 'roomList']
can_publish bool @[json: 'canPublish'] // can_publish bool @[json: 'canPublish']
can_publish_data bool @[json: 'canPublishData'] // can_publish_data bool @[json: 'canPublishData']
can_subscribe bool @[json: 'canSubscribe'] // can_subscribe bool @[json: 'canSubscribe']
} // }
// SIPGrant struct placeholder // SIPGrant struct placeholder
struct SIPGrant {} struct SIPGrant {}
// AccessToken class // // AccessToken class
pub struct AccessToken { // pub struct AccessToken {
mut: // mut:
api_key string // api_key string
api_secret string // api_secret string
grants ClaimGrants // grants ClaimGrants
identity string // identity string
ttl int // ttl int
} // }
// Method to add a video grant to the token // Method to add a video grant to the token
pub fn (mut token AccessToken) add_video_grant(grant VideoGrant) { // pub fn (mut token AccessToken) add_video_grant(grant VideoGrant) {
token.grants.video = grant // token.grants.video = grant
} // }
// Method to generate a JWT token // // Method to generate a JWT token
pub fn (token AccessToken) to_jwt() !string { // pub fn (token AccessToken) to_jwt() !string {
// Create JWT payload // // Create JWT payload
payload := json.encode(token.grants) // payload := json.encode(token.grants)
println('payload: ${payload}') // println('payload: ${payload}')
// Create JWT header // // Create JWT header
header := '{"alg":"HS256","typ":"JWT"}' // header := '{"alg":"HS256","typ":"JWT"}'
// Encode header and payload in base64 // // Encode header and payload in base64
header_encoded := base64.url_encode_str(header) // header_encoded := base64.url_encode_str(header)
payload_encoded := base64.url_encode_str(payload) // payload_encoded := base64.url_encode_str(payload)
// Create the unsigned token // // Create the unsigned token
unsigned_token := '${header_encoded}.${payload_encoded}' // unsigned_token := '${header_encoded}.${payload_encoded}'
// Create the HMAC-SHA256 signature // // Create the HMAC-SHA256 signature
signature := hmac.new(token.api_secret.bytes(), unsigned_token.bytes(), sha256.sum, // signature := hmac.new(token.api_secret.bytes(), unsigned_token.bytes(), sha256.sum,
sha256.block_size) // sha256.block_size)
// Encode the signature in base64 // // Encode the signature in base64
signature_encoded := base64.url_encode(signature) // signature_encoded := base64.url_encode(signature)
// Create the final JWT // // Create the final JWT
jwt := '${unsigned_token}.${signature_encoded}' // jwt := '${unsigned_token}.${signature_encoded}'
return jwt // return jwt
} // }

View File

@@ -15,8 +15,8 @@ pub const default_url = 'http://localhost:8990'
@[heap] @[heap]
pub struct MyceliumRPC { pub struct MyceliumRPC {
pub mut: pub mut:
name string = 'default' name string = 'default'
url string = default_url // RPC server URL url string = default_url // RPC server URL
// rpc_client ?&jsonrpc.Client @[skip] // rpc_client ?&jsonrpc.Client @[skip]
} }

View File

@@ -24,8 +24,8 @@ pub fn new(args FactoryArgs) !&TraefikManager {
mut redis := redisclient.core_get(redisclient.get_redis_url(args.redis_url)!)! mut redis := redisclient.core_get(redisclient.get_redis_url(args.redis_url)!)!
mut manager := &TraefikManager{ mut manager := &TraefikManager{
name: name name: name
redis: redis redis: redis
config: osal_traefik.new_traefik_config() config: osal_traefik.new_traefik_config()
} }

View File

@@ -23,9 +23,9 @@ pub mut:
@[params] @[params]
pub struct RouterAddArgs { pub struct RouterAddArgs {
pub mut: pub mut:
name string @[required] name string @[required]
rule string @[required] rule string @[required]
service string @[required] service string @[required]
entrypoints []string entrypoints []string
middlewares []string middlewares []string
tls bool tls bool
@@ -43,8 +43,8 @@ pub mut:
@[params] @[params]
pub struct MiddlewareAddArgs { pub struct MiddlewareAddArgs {
pub mut: pub mut:
name string @[required] name string @[required]
typ string @[required] typ string @[required]
settings map[string]string settings map[string]string
} }
@@ -78,7 +78,7 @@ pub fn (mut tm TraefikManager) service_add(args ServiceAddArgs) ! {
} }
tm.config.add_service( tm.config.add_service(
name: texttools.name_fix(args.name) name: texttools.name_fix(args.name)
load_balancer: osal_traefik.LoadBalancerConfig{ load_balancer: osal_traefik.LoadBalancerConfig{
servers: servers servers: servers
} }

View File

@@ -16,9 +16,9 @@ pub fn scan(args_ GeneratorArgs) ! {
// now walk over all directories, find .heroscript // now walk over all directories, find .heroscript
mut pathroot := pathlib.get_dir(path: args.path, create: false)! mut pathroot := pathlib.get_dir(path: args.path, create: false)!
mut plist := pathroot.list( mut plist := pathroot.list(
recursive: true recursive: true
ignore_default: false ignore_default: false
regex: ['.heroscript'] regex: ['.heroscript']
)! )!
for mut p in plist.paths { for mut p in plist.paths {

View File

@@ -8,12 +8,12 @@ import freeflowuniverse.herolib.ui.console
@[params] @[params]
pub struct ListArgs { pub struct ListArgs {
pub mut: pub mut:
regex []string regex []string
recursive bool = true recursive bool = true
ignore_default bool = true // ignore files starting with . and _ ignore_default bool = true // ignore files starting with . and _
include_links bool // wether to include links in list include_links bool // wether to include links in list
dirs_only bool dirs_only bool
files_only bool files_only bool
} }
// the result of pathlist // the result of pathlist
@@ -54,12 +54,12 @@ pub fn (mut path Path) list(args_ ListArgs) !PathList {
r << re r << re
} }
mut args := ListArgsInternal{ mut args := ListArgsInternal{
regex: r regex: r
recursive: args_.recursive recursive: args_.recursive
ignore_default: args_.ignore_default ignore_default: args_.ignore_default
dirs_only: args_.dirs_only dirs_only: args_.dirs_only
files_only: args_.files_only files_only: args_.files_only
include_links: args_.include_links include_links: args_.include_links
} }
paths := path.list_internal(args)! paths := path.list_internal(args)!
mut pl := PathList{ mut pl := PathList{
@@ -72,12 +72,12 @@ pub fn (mut path Path) list(args_ ListArgs) !PathList {
@[params] @[params]
pub struct ListArgsInternal { pub struct ListArgsInternal {
mut: mut:
regex []regex.RE // only put files in which follow one of the regexes regex []regex.RE // only put files in which follow one of the regexes
recursive bool = true recursive bool = true
ignore_default bool = true // ignore files starting with . and _ ignore_default bool = true // ignore files starting with . and _
dirs_only bool dirs_only bool
files_only bool files_only bool
include_links bool include_links bool
} }
fn (mut path Path) list_internal(args ListArgsInternal) ![]Path { fn (mut path Path) list_internal(args ListArgsInternal) ![]Path {

View File

@@ -40,12 +40,6 @@ pub fn (key SSHKey) private_key() !string {
return content return content
} }
module core
import freeflowuniverse.herolib.core.pathlib
import os
@[params] @[params]
pub struct SSHConfig { pub struct SSHConfig {
pub: pub:

View File

@@ -24,6 +24,6 @@ pub:
pub fn new(args LinuxNewArgs) !LinuxFactory { pub fn new(args LinuxNewArgs) !LinuxFactory {
mut t := LinuxFactory{ mut t := LinuxFactory{
username: args.username username: args.username
} }
return t return t
} }

View File

@@ -29,14 +29,14 @@ fn play_user_create(mut plbook PlayBook, mut lf LinuxFactory) ! {
mut p := action.params mut p := action.params
mut args := UserCreateArgs{ mut args := UserCreateArgs{
name: p.get('name')! name: p.get('name')!
giteakey: p.get_default('giteakey', '')! giteakey: p.get_default('giteakey', '')!
giteaurl: p.get_default('giteaurl', '')! giteaurl: p.get_default('giteaurl', '')!
passwd: p.get_default('passwd', '')! passwd: p.get_default('passwd', '')!
description: p.get_default('description', '')! description: p.get_default('description', '')!
email: p.get_default('email', '')! email: p.get_default('email', '')!
tel: p.get_default('tel', '')! tel: p.get_default('tel', '')!
sshkey: p.get_default('sshkey', '')! // SSH public key sshkey: p.get_default('sshkey', '')! // SSH public key
} }
lf.user_create(args)! lf.user_create(args)!
@@ -66,9 +66,9 @@ fn play_sshkey_create(mut plbook PlayBook, mut lf LinuxFactory) ! {
mut p := action.params mut p := action.params
mut args := SSHKeyCreateArgs{ mut args := SSHKeyCreateArgs{
username: p.get('username')! username: p.get('username')!
sshkey_name: p.get('sshkey_name')! sshkey_name: p.get('sshkey_name')!
sshkey_pub: p.get_default('sshkey_pub', '')! sshkey_pub: p.get_default('sshkey_pub', '')!
sshkey_priv: p.get_default('sshkey_priv', '')! sshkey_priv: p.get_default('sshkey_priv', '')!
} }
@@ -84,7 +84,7 @@ fn play_sshkey_delete(mut plbook PlayBook, mut lf LinuxFactory) ! {
mut p := action.params mut p := action.params
mut args := SSHKeyDeleteArgs{ mut args := SSHKeyDeleteArgs{
username: p.get('username')! username: p.get('username')!
sshkey_name: p.get('sshkey_name')! sshkey_name: p.get('sshkey_name')!
} }

View File

@@ -0,0 +1,12 @@
# Auto-start ssh-agent if not running
SSH_AGENT_PID_FILE="$HOME/.ssh/agent.pid"
SSH_AUTH_SOCK_FILE="$HOME/.ssh/agent.sock"
chown "$NEWUSER":"$NEWUSER" "$PROFILE_SCRIPT"
chmod 644 "$PROFILE_SCRIPT"
# --- source it on login ---
#TODO should be done in vcode
if ! grep -q ".profile_sshagent" "$USERHOME/.bashrc"; then
echo "[ -f ~/.profile_sshagent ] && source ~/.profile_sshagent" >> "$USERHOME/.bashrc"
fi

View File

@@ -57,19 +57,4 @@ chown root:ourworld /code
chmod 2775 /code # rwx for user+group, SGID bit so new files inherit group chmod 2775 /code # rwx for user+group, SGID bit so new files inherit group
echo "✅ /code prepared (group=ourworld, rwx for group, SGID bit set)" echo "✅ /code prepared (group=ourworld, rwx for group, SGID bit set)"
# --- create login helper script for ssh-agent ---
PROFILE_SCRIPT="$USERHOME/.profile_sshagent"
cat > "$PROFILE_SCRIPT" <<'EOF'
# Auto-start ssh-agent if not running
SSH_AGENT_PID_FILE="$HOME/.ssh/agent.pid"
SSH_AUTH_SOCK_FILE="$HOME/.ssh/agent.sock"
chown "$NEWUSER":"$NEWUSER" "$PROFILE_SCRIPT"
chmod 644 "$PROFILE_SCRIPT"
# --- source it on login ---
if ! grep -q ".profile_sshagent" "$USERHOME/.bashrc"; then
echo "[ -f ~/.profile_sshagent ] && source ~/.profile_sshagent" >> "$USERHOME/.bashrc"
fi
echo "🎉 Setup complete for user $NEWUSER" echo "🎉 Setup complete for user $NEWUSER"

View File

@@ -119,7 +119,9 @@ pub fn (mut lf LinuxFactory) sshkey_create(args SSHKeyCreateArgs) ! {
} else { } else {
// Generate new SSH key (modern ed25519) // Generate new SSH key (modern ed25519)
key_path := '${ssh_dir}/${args.sshkey_name}' key_path := '${ssh_dir}/${args.sshkey_name}'
osal.exec(cmd: 'ssh-keygen -t ed25519 -f ${key_path} -N "" -C "${args.username}@$(hostname)"')! osal.exec(
cmd: 'ssh-keygen -t ed25519 -f ${key_path} -N "" -C "${args.username}@$(hostname)"'
)!
console.print_green(' New SSH key generated for ${args.username}') console.print_green(' New SSH key generated for ${args.username}')
} }
@@ -175,12 +177,12 @@ fn (mut lf LinuxFactory) save_user_config(args UserCreateArgs) ! {
} }
new_config := UserConfig{ new_config := UserConfig{
name: args.name name: args.name
giteakey: args.giteakey giteakey: args.giteakey
giteaurl: args.giteaurl giteaurl: args.giteaurl
email: args.email email: args.email
description: args.description description: args.description
tel: args.tel tel: args.tel
} }
if found_idx >= 0 { if found_idx >= 0 {
@@ -201,7 +203,7 @@ fn (mut lf LinuxFactory) remove_user_config(username string) ! {
config_path := '${config_dir}/myconfig.json' config_path := '${config_dir}/myconfig.json'
if !os.exists(config_path) { if !os.exists(config_path) {
return // Nothing to remove return
} }
content := osal.file_read(config_path)! content := osal.file_read(config_path)!
@@ -243,7 +245,9 @@ fn (mut lf LinuxFactory) create_user_system(args UserCreateArgs) ! {
// Ensure ourworld group exists // Ensure ourworld group exists
group_check := osal.exec(cmd: 'getent group ourworld', raise_error: false) or { group_check := osal.exec(cmd: 'getent group ourworld', raise_error: false) or {
osal.Job{ exit_code: 1 } osal.Job{
exit_code: 1
}
} }
if group_check.exit_code != 0 { if group_check.exit_code != 0 {
console.print_item(' Creating group ourworld') console.print_item(' Creating group ourworld')
@@ -284,58 +288,9 @@ fn (mut lf LinuxFactory) create_ssh_agent_profile(username string) ! {
user_home := '/home/${username}' user_home := '/home/${username}'
profile_script := '${user_home}/.profile_sshagent' profile_script := '${user_home}/.profile_sshagent'
script_content := '# Auto-start ssh-agent if not running // script_content := ''
SSH_AGENT_PID_FILE="$HOME/.ssh/agent.pid"
SSH_AUTH_SOCK_FILE="$HOME/.ssh/agent.sock"
# Function to start ssh-agent panic('implement')
start_ssh_agent() {
mkdir -p "$HOME/.ssh"
chmod 700 "$HOME/.ssh"
# Start ssh-agent and save connection info
ssh-agent -s > "$SSH_AGENT_PID_FILE"
source "$SSH_AGENT_PID_FILE"
# Save socket path for future sessions
echo "$SSH_AUTH_SOCK" > "$SSH_AUTH_SOCK_FILE"
# Load all private keys found in ~/.ssh
if [ -d "$HOME/.ssh" ]; then
for KEY in "$HOME"/.ssh/*; do
if [ -f "$KEY" ] && [ ! "${KEY##*.}" = "pub" ] && grep -q "PRIVATE KEY" "$KEY" 2>/dev/null; then
'ssh-' + 'add "$KEY" >/dev/null 2>&1 && echo "🔑 Loaded key: $(basename $KEY)"'
fi
done
fi
}
# Check if ssh-agent is running
if [ -f "$SSH_AGENT_PID_FILE" ]; then
source "$SSH_AGENT_PID_FILE" >/dev/null 2>&1
# Test if agent is responsive
if ! ('ssh-' + 'add -l >/dev/null 2>&1'); then
start_ssh_agent
else
# Agent is running, restore socket path
if [ -f "$SSH_AUTH_SOCK_FILE" ]; then
export SSH_AUTH_SOCK=$(cat "$SSH_AUTH_SOCK_FILE")
fi
fi
else
start_ssh_agent
fi
# For interactive shells
if [[ $- == *i* ]]; then
echo "🔑 SSH Agent ready at $SSH_AUTH_SOCK"
# Show loaded keys
KEY_COUNT=$('ssh-' + 'add -l 2>/dev/null | wc -l')
if [ "$KEY_COUNT" -gt 0 ]; then
echo "🔑 $KEY_COUNT SSH key(s) loaded"
fi
fi
'
osal.file_write(profile_script, script_content)! osal.file_write(profile_script, script_content)!
osal.exec(cmd: 'chown ${username}:${username} ${profile_script}')! osal.exec(cmd: 'chown ${username}:${username} ${profile_script}')!

View File

@@ -96,9 +96,7 @@ fn sshkey_delete(mut agent SSHAgent, name string) ! {
fn sshkey_load(mut agent SSHAgent, name string) ! { fn sshkey_load(mut agent SSHAgent, name string) ! {
console.print_header('Loading SSH key: ${name}') console.print_header('Loading SSH key: ${name}')
mut key := agent.get(name: name) or { mut key := agent.get(name: name) or { return error('SSH key "${name}" not found') }
return error('SSH key "${name}" not found')
}
if key.loaded { if key.loaded {
console.print_debug('SSH key "${name}" is already loaded') console.print_debug('SSH key "${name}" is already loaded')
@@ -113,18 +111,12 @@ fn sshkey_load(mut agent SSHAgent, name string) ! {
fn sshkey_check(mut agent SSHAgent, name string) ! { fn sshkey_check(mut agent SSHAgent, name string) ! {
console.print_header('Checking SSH key: ${name}') console.print_header('Checking SSH key: ${name}')
mut key := agent.get(name: name) or { mut key := agent.get(name: name) or { return error('SSH key "${name}" not found') }
return error('SSH key "${name}" not found')
}
// Check if key files exist // Check if key files exist
key_path := key.keypath() or { key_path := key.keypath() or { return error('Private key file not found for "${name}"') }
return error('Private key file not found for "${name}"')
}
key_pub_path := key.keypath_pub() or { key_pub_path := key.keypath_pub() or { return error('Public key file not found for "${name}"') }
return error('Public key file not found for "${name}"')
}
if !key_path.exists() { if !key_path.exists() {
return error('Private key file does not exist: ${key_path.path}') return error('Private key file does not exist: ${key_path.path}')
@@ -157,9 +149,7 @@ fn remote_copy(mut agent SSHAgent, node_addr string, key_name string) ! {
console.print_header('Copying SSH key "${key_name}" to ${node_addr}') console.print_header('Copying SSH key "${key_name}" to ${node_addr}')
// Get the key // Get the key
mut key := agent.get(name: key_name) or { mut key := agent.get(name: key_name) or { return error('SSH key "${key_name}" not found') }
return error('SSH key "${key_name}" not found')
}
// Create builder node // Create builder node
mut b := builder.new()! mut b := builder.new()!

View File

@@ -92,9 +92,7 @@ pub fn (mut agent SSHAgent) verify_key_access(mut node builder.Node, key_name st
} }
// Test basic connectivity // Test basic connectivity
result := node.exec_silent('echo "SSH key verification successful"') or { result := node.exec_silent('echo "SSH key verification successful"') or { return false }
return false
}
return result.contains('SSH key verification successful') return result.contains('SSH key verification successful')
} }

View File

@@ -44,7 +44,7 @@ fn parse_window_name(name string) !ParsedWindowName {
} }
return ParsedWindowName{ return ParsedWindowName{
session: texttools.name_fix(parts[0]) session: texttools.name_fix(parts[0])
window: texttools.name_fix(parts[1]) window: texttools.name_fix(parts[1])
} }
} }
@@ -55,8 +55,8 @@ fn parse_pane_name(name string) !ParsedPaneName {
} }
return ParsedPaneName{ return ParsedPaneName{
session: texttools.name_fix(parts[0]) session: texttools.name_fix(parts[0])
window: texttools.name_fix(parts[1]) window: texttools.name_fix(parts[1])
pane: texttools.name_fix(parts[2]) pane: texttools.name_fix(parts[2])
} }
} }
@@ -68,7 +68,7 @@ fn play_session_create(mut plbook PlayBook, mut tmux_instance Tmux) ! {
reset := p.get_default_false('reset') reset := p.get_default_false('reset')
tmux_instance.session_create( tmux_instance.session_create(
name: session_name name: session_name
reset: reset reset: reset
)! )!
@@ -118,9 +118,9 @@ fn play_window_create(mut plbook PlayBook, mut tmux_instance Tmux) ! {
} }
session.window_new( session.window_new(
name: parsed.window name: parsed.window
cmd: cmd cmd: cmd
env: env env: env
reset: reset reset: reset
)! )!
@@ -184,7 +184,12 @@ fn play_pane_kill(mut plbook PlayBook, mut tmux_instance Tmux) ! {
// Kill the active pane in the window // Kill the active pane in the window
if pane := window.pane_active() { if pane := window.pane_active() {
tmux_cmd := 'tmux kill-pane -t ${session.name}:@${window.id}.%${pane.id}' tmux_cmd := 'tmux kill-pane -t ${session.name}:@${window.id}.%${pane.id}'
osal.exec(cmd: tmux_cmd, stdout: false, name: 'tmux_pane_kill', ignore_error: true)! osal.exec(
cmd: tmux_cmd
stdout: false
name: 'tmux_pane_kill'
ignore_error: true
)!
} }
} }
} }

View File

@@ -14,7 +14,6 @@ pub mut:
sessionid string // unique link to job sessionid string // unique link to job
} }
// get session (session has windows) . // get session (session has windows) .
// returns none if not found // returns none if not found
pub fn (mut t Tmux) session_get(name_ string) !&Session { pub fn (mut t Tmux) session_get(name_ string) !&Session {
@@ -56,8 +55,6 @@ pub mut:
reset bool reset bool
} }
// create session, if reset will re-create // create session, if reset will re-create
pub fn (mut t Tmux) session_create(args SessionCreateArgs) !&Session { pub fn (mut t Tmux) session_create(args SessionCreateArgs) !&Session {
name := texttools.name_fix(args.name) name := texttools.name_fix(args.name)
@@ -83,7 +80,6 @@ pub fn (mut t Tmux) session_create(args SessionCreateArgs) !&Session {
return s return s
} }
@[params] @[params]
pub struct TmuxNewArgs { pub struct TmuxNewArgs {
sessionid string sessionid string
@@ -119,14 +115,13 @@ pub fn (mut t Tmux) window_new(args WindowNewArgs) !&Window {
// Create window in session // Create window in session
return session.window_new( return session.window_new(
name: args.name name: args.name
cmd: args.cmd cmd: args.cmd
env: args.env env: args.env
reset: args.reset reset: args.reset
)! )!
} }
pub fn (mut t Tmux) stop() ! { pub fn (mut t Tmux) stop() ! {
$if debug { $if debug {
console.print_debug('Stopping tmux...') console.print_debug('Stopping tmux...')
@@ -156,7 +151,6 @@ pub fn (mut t Tmux) start() ! {
t.scan()! t.scan()!
} }
// print list of tmux sessions // print list of tmux sessions
pub fn (mut t Tmux) list_print() { pub fn (mut t Tmux) list_print() {
// os.log('TMUX - Start listing ....') // os.log('TMUX - Start listing ....')

View File

@@ -10,141 +10,134 @@ import freeflowuniverse.herolib.ui.console
@[heap] @[heap]
struct Pane { struct Pane {
pub mut: pub mut:
window &Window @[str: skip] window &Window @[str: skip]
id int // pane id (e.g., %1, %2) id int // pane id (e.g., %1, %2)
pid int // process id pid int // process id
active bool // is this the active pane active bool // is this the active pane
cmd string // command running in pane cmd string // command running in pane
env map[string]string env map[string]string
created_at time.Time created_at time.Time
last_output_offset int // for tracking new logs last_output_offset int // for tracking new logs
} }
pub fn (mut p Pane) stats() !ProcessStats { pub fn (mut p Pane) stats() !ProcessStats {
if p.pid == 0 { if p.pid == 0 {
return ProcessStats{} return ProcessStats{}
} }
// Use ps command to get CPU and memory stats // Use ps command to get CPU and memory stats
cmd := 'ps -p ${p.pid} -o %cpu,%mem,rss --no-headers' cmd := 'ps -p ${p.pid} -o %cpu,%mem,rss --no-headers'
result := osal.execute_silent(cmd) or { result := osal.execute_silent(cmd) or {
return error('Cannot get stats for PID ${p.pid}: ${err}') return error('Cannot get stats for PID ${p.pid}: ${err}')
} }
if result.trim_space() == '' { if result.trim_space() == '' {
return error('Process ${p.pid} not found') return error('Process ${p.pid} not found')
} }
parts := result.trim_space().split_any(' \t').filter(it != '') parts := result.trim_space().split_any(' \t').filter(it != '')
if parts.len < 3 { if parts.len < 3 {
return error('Invalid ps output: ${result}') return error('Invalid ps output: ${result}')
} }
return ProcessStats{ return ProcessStats{
cpu_percent: parts[0].f64() cpu_percent: parts[0].f64()
memory_percent: parts[1].f64() memory_percent: parts[1].f64()
memory_bytes: parts[2].u64() * 1024 // ps returns KB, convert to bytes memory_bytes: parts[2].u64() * 1024 // ps returns KB, convert to bytes
} }
} }
pub struct TMuxLogEntry { pub struct TMuxLogEntry {
pub mut: pub mut:
content string content string
timestamp time.Time timestamp time.Time
offset int offset int
} }
pub fn (mut p Pane) logs_get_new(reset bool) ![]TMuxLogEntry { pub fn (mut p Pane) logs_get_new(reset bool) ![]TMuxLogEntry {
if reset {
p.last_output_offset = 0
}
// Capture pane content with line numbers
cmd := 'tmux capture-pane -t ${p.window.session.name}:@${p.window.id}.%${p.id} -S ${p.last_output_offset} -p'
result := osal.execute_silent(cmd) or { return error('Cannot capture pane output: ${err}') }
if reset{ lines := result.split_into_lines()
p.last_output_offset = 0 mut entries := []TMuxLogEntry{}
}
// Capture pane content with line numbers
cmd := 'tmux capture-pane -t ${p.window.session.name}:@${p.window.id}.%${p.id} -S ${p.last_output_offset} -p'
result := osal.execute_silent(cmd) or {
return error('Cannot capture pane output: ${err}')
}
lines := result.split_into_lines() mut i := 0
mut entries := []TMuxLogEntry{} for line in lines {
if line.trim_space() != '' {
mut i:= 0 entries << TMuxLogEntry{
for line in lines { content: line
if line.trim_space() != '' { timestamp: time.now()
entries << TMuxLogEntry{ offset: p.last_output_offset + i + 1
content: line }
timestamp: time.now() }
offset: p.last_output_offset + i + 1 }
} // Update offset to avoid duplicates next time
} if entries.len > 0 {
} p.last_output_offset = entries.last().offset
// Update offset to avoid duplicates next time }
if entries.len > 0 { return entries
p.last_output_offset = entries.last().offset
}
return entries
} }
pub fn (mut p Pane) exit_status() !ProcessStatus { pub fn (mut p Pane) exit_status() !ProcessStatus {
// Get the last few lines to see if there's an exit status // Get the last few lines to see if there's an exit status
logs := p.logs_all()! logs := p.logs_all()!
lines := logs.split_into_lines() lines := logs.split_into_lines()
// Look for shell prompt indicating command finished // Look for shell prompt indicating command finished
for line in lines.reverse() { for line in lines.reverse() {
line_clean := line.trim_space() line_clean := line.trim_space()
if line_clean.contains('$') || line_clean.contains('#') || line_clean.contains('>') { if line_clean.contains('$') || line_clean.contains('#') || line_clean.contains('>') {
// Found shell prompt, command likely finished // Found shell prompt, command likely finished
// Could also check for specific exit codes in history // Could also check for specific exit codes in history
return .finished_ok return .finished_ok
} }
} }
return .finished_error return .finished_error
} }
pub fn (mut p Pane) logs_all() !string { pub fn (mut p Pane) logs_all() !string {
cmd := 'tmux capture-pane -t ${p.window.session.name}:@${p.window.id}.%${p.id} -S -2000 -p' cmd := 'tmux capture-pane -t ${p.window.session.name}:@${p.window.id}.%${p.id} -S -2000 -p'
return osal.execute_silent(cmd) or { return osal.execute_silent(cmd) or { error('Cannot capture pane output: ${err}') }
error('Cannot capture pane output: ${err}')
}
} }
// Fix the output_wait method to use correct method name // Fix the output_wait method to use correct method name
pub fn (mut p Pane) output_wait(c_ string, timeoutsec int) ! { pub fn (mut p Pane) output_wait(c_ string, timeoutsec int) ! {
mut t := ourtime.now() mut t := ourtime.now()
start := t.unix() start := t.unix()
c := c_.replace('\n', '') c := c_.replace('\n', '')
for i in 0 .. 2000 { for i in 0 .. 2000 {
entries := p.logs_get_new(reset: false)! entries := p.logs_get_new(reset: false)!
for entry in entries { for entry in entries {
if entry.content.replace('\n', '').contains(c) { if entry.content.replace('\n', '').contains(c) {
return return
} }
} }
mut t2 := ourtime.now() mut t2 := ourtime.now()
if t2.unix() > start + timeoutsec { if t2.unix() > start + timeoutsec {
return error('timeout on output wait for tmux.\n${p} .\nwaiting for:\n${c}') return error('timeout on output wait for tmux.\n${p} .\nwaiting for:\n${c}')
} }
time.sleep(100 * time.millisecond) time.sleep(100 * time.millisecond)
} }
} }
// Get process information for this pane and all its children // Get process information for this pane and all its children
pub fn (mut p Pane) processinfo() !osal.ProcessMap { pub fn (mut p Pane) processinfo() !osal.ProcessMap {
if p.pid == 0 { if p.pid == 0 {
return error('Pane has no associated process (pid is 0)') return error('Pane has no associated process (pid is 0)')
} }
return osal.processinfo_with_children(p.pid)! return osal.processinfo_with_children(p.pid)!
} }
// Get process information for just this pane's main process // Get process information for just this pane's main process
pub fn (mut p Pane) processinfo_main() !osal.ProcessInfo { pub fn (mut p Pane) processinfo_main() !osal.ProcessInfo {
if p.pid == 0 { if p.pid == 0 {
return error('Pane has no associated process (pid is 0)') return error('Pane has no associated process (pid is 0)')
} }
return osal.processinfo_get(p.pid)! return osal.processinfo_get(p.pid)!
} }

View File

@@ -1,21 +1,15 @@
module tmux module tmux
pub struct ProcessStats { pub struct ProcessStats {
pub mut: pub mut:
cpu_percent f64 cpu_percent f64
memory_bytes u64 memory_bytes u64
memory_percent f64 memory_percent f64
} }
enum ProcessStatus { enum ProcessStatus {
running running
finished_ok finished_ok
finished_error finished_error
not_found not_found
} }

View File

@@ -6,66 +6,66 @@ import freeflowuniverse.herolib.ui.console
import time import time
fn (mut t Tmux) scan_add(line string) !&Pane { fn (mut t Tmux) scan_add(line string) !&Pane {
// Parse the line to get session, window, and pane info // Parse the line to get session, window, and pane info
line_arr := line.split('|') line_arr := line.split('|')
session_name := line_arr[0] session_name := line_arr[0]
window_name := line_arr[1] window_name := line_arr[1]
window_id := line_arr[2] window_id := line_arr[2]
pane_active := line_arr[3] pane_active := line_arr[3]
pane_id := line_arr[4] pane_id := line_arr[4]
pane_pid := line_arr[5] pane_pid := line_arr[5]
pane_start_command := line_arr[6] or { '' } pane_start_command := line_arr[6] or { '' }
wid := (window_id.replace('@', '')).int() wid := (window_id.replace('@', '')).int()
pid := (pane_id.replace('%', '')).int() pid := (pane_id.replace('%', '')).int()
mut s := t.session_get(session_name)! mut s := t.session_get(session_name)!
// Get or create window // Get or create window
mut w := if s.window_exist(name: window_name, id: wid) { mut w := if s.window_exist(name: window_name, id: wid) {
s.window_get(name: window_name, id: wid)! s.window_get(name: window_name, id: wid)!
} else { } else {
mut new_w := Window{ mut new_w := Window{
session: s session: s
name: texttools.name_fix(window_name) name: texttools.name_fix(window_name)
id: wid id: wid
panes: []&Pane{} panes: []&Pane{}
} }
s.windows << &new_w s.windows << &new_w
&new_w &new_w
} }
// Create or update pane // Create or update pane
mut p := Pane{ mut p := Pane{
window: w window: w
id: pid id: pid
pid: pane_pid.int() pid: pane_pid.int()
active: pane_active == '1' active: pane_active == '1'
cmd: pane_start_command cmd: pane_start_command
created_at: time.now() created_at: time.now()
} }
// Check if pane already exists // Check if pane already exists
mut found := false mut found := false
for mut existing_pane in w.panes { for mut existing_pane in w.panes {
if existing_pane.id == pid { if existing_pane.id == pid {
existing_pane.pid = p.pid existing_pane.pid = p.pid
existing_pane.active = p.active existing_pane.active = p.active
existing_pane.cmd = p.cmd existing_pane.cmd = p.cmd
found = true found = true
break break
} }
} }
if !found { if !found {
w.panes << &p w.panes << &p
} }
return &p return &p
} }
// scan the system to detect sessions . // scan the system to detect sessions .
//TODO needs to be done differently, here only find the sessions, then per session call the scan() which will find the windows, call scan() there as well ... // TODO needs to be done differently, here only find the sessions, then per session call the scan() which will find the windows, call scan() there as well ...
pub fn (mut t Tmux) scan() ! { pub fn (mut t Tmux) scan() ! {
// os.log('TMUX - Scanning ....') // os.log('TMUX - Scanning ....')

View File

@@ -21,88 +21,87 @@ pub mut:
env map[string]string env map[string]string
reset bool reset bool
} }
@[params] @[params]
pub struct WindowGetArgs { pub struct WindowGetArgs {
pub mut: pub mut:
name string name string
id int id int
} }
pub fn (mut s Session) create() ! { pub fn (mut s Session) create() ! {
// Check if session already exists // Check if session already exists
cmd_check := "tmux has-session -t ${s.name}" cmd_check := 'tmux has-session -t ${s.name}'
check_result := osal.exec(cmd: cmd_check, stdout: false, ignore_error: true) or { check_result := osal.exec(cmd: cmd_check, stdout: false, ignore_error: true) or {
// Session doesn't exist, this is expected // Session doesn't exist, this is expected
osal.Job{} osal.Job{}
} }
if check_result.exit_code == 0 { if check_result.exit_code == 0 {
return error('duplicate session: ${s.name}') return error('duplicate session: ${s.name}')
} }
// Create new session // Create new session
cmd := "tmux new-session -d -s ${s.name}" cmd := 'tmux new-session -d -s ${s.name}'
osal.exec(cmd: cmd, stdout: false, name: 'tmux_session_create') or { osal.exec(cmd: cmd, stdout: false, name: 'tmux_session_create') or {
return error("Can't create session ${s.name}: ${err}") return error("Can't create session ${s.name}: ${err}")
} }
} }
//load info from reality // load info from reality
pub fn (mut s Session) scan() ! { pub fn (mut s Session) scan() ! {
// Get current windows from tmux for this session // Get current windows from tmux for this session
cmd := "tmux list-windows -t ${s.name} -F '#{window_name}|#{window_id}|#{window_active}'" cmd := "tmux list-windows -t ${s.name} -F '#{window_name}|#{window_id}|#{window_active}'"
result := osal.execute_silent(cmd) or { result := osal.execute_silent(cmd) or {
if err.msg().contains('session not found') { if err.msg().contains('session not found') {
return // Session doesn't exist anymore return
} }
return error('Cannot list windows for session ${s.name}: ${err}') return error('Cannot list windows for session ${s.name}: ${err}')
} }
mut current_windows := map[string]bool{} mut current_windows := map[string]bool{}
for line in result.split_into_lines() { for line in result.split_into_lines() {
if line.contains('|') { if line.contains('|') {
parts := line.split('|') parts := line.split('|')
if parts.len >= 2 { if parts.len >= 2 {
window_name := texttools.name_fix(parts[0]) window_name := texttools.name_fix(parts[0])
window_id := parts[1].replace('@', '').int() window_id := parts[1].replace('@', '').int()
window_active := parts[2] == '1' window_active := parts[2] == '1'
current_windows[window_name] = true current_windows[window_name] = true
// Update existing window or create new one // Update existing window or create new one
mut found := false mut found := false
for mut w in s.windows { for mut w in s.windows {
if w.name == window_name { if w.name == window_name {
w.id = window_id w.id = window_id
w.active = window_active w.active = window_active
w.scan()! // Scan panes for this window w.scan()! // Scan panes for this window
found = true found = true
break break
} }
} }
if !found { if !found {
mut new_window := Window{ mut new_window := Window{
session: &s session: &s
name: window_name name: window_name
id: window_id id: window_id
active: window_active active: window_active
panes: []&Pane{} panes: []&Pane{}
env: map[string]string{} env: map[string]string{}
} }
new_window.scan()! // Scan panes for new window new_window.scan()! // Scan panes for new window
s.windows << &new_window s.windows << &new_window
} }
} }
} }
} }
// Remove windows that no longer exist in tmux // Remove windows that no longer exist in tmux
s.windows = s.windows.filter(current_windows[it.name] == true) s.windows = s.windows.filter(current_windows[it.name] == true)
} }
// window_name is the name of the window in session main (will always be called session main) // window_name is the name of the window in session main (will always be called session main)
// cmd to execute e.g. bash file // cmd to execute e.g. bash file
// environment arguments to use // environment arguments to use
@@ -143,10 +142,6 @@ pub fn (mut s Session) window_new(args WindowArgs) !Window {
return w return w
} }
// get all windows as found in a session // get all windows as found in a session
pub fn (mut s Session) windows_get() []&Window { pub fn (mut s Session) windows_get() []&Window {
mut res := []&Window{} mut res := []&Window{}
@@ -179,14 +174,14 @@ pub fn (mut s Session) str() string {
} }
pub fn (mut s Session) stats() !ProcessStats { pub fn (mut s Session) stats() !ProcessStats {
mut total := ProcessStats{} mut total := ProcessStats{}
for mut window in s.windows { for mut window in s.windows {
stats := window.stats() or { continue } stats := window.stats() or { continue }
total.cpu_percent += stats.cpu_percent total.cpu_percent += stats.cpu_percent
total.memory_bytes += stats.memory_bytes total.memory_bytes += stats.memory_bytes
total.memory_percent += stats.memory_percent total.memory_percent += stats.memory_percent
} }
return total return total
} }
// pub fn (mut s Session) activate()! { // pub fn (mut s Session) activate()! {
@@ -208,8 +203,6 @@ pub fn (mut s Session) stats() !ProcessStats {
// } // }
// } // }
fn (mut s Session) window_exist(args_ WindowGetArgs) bool { fn (mut s Session) window_exist(args_ WindowGetArgs) bool {
mut args := args_ mut args := args_
s.window_get(args) or { return false } s.window_get(args) or { return false }
@@ -249,7 +242,6 @@ pub fn (mut s Session) window_delete(args_ WindowGetArgs) ! {
s.windows.delete(i) // i is now the one in the list which needs to be removed s.windows.delete(i) // i is now the one in the list which needs to be removed
} }
pub fn (mut s Session) restart() ! { pub fn (mut s Session) restart() ! {
s.stop()! s.stop()!
s.create()! s.create()!

View File

@@ -45,37 +45,37 @@ fn test_stop() ! {
} }
fn test_windows_get() ! { fn test_windows_get() ! {
mut tmux := new()! mut tmux := new()!
tmux.start()! tmux.start()!
// After start, scan to get the initial session // After start, scan to get the initial session
tmux.scan()! tmux.scan()!
windows := tmux.windows_get() windows := tmux.windows_get()
assert windows.len >= 0 // At least the default session should exist assert windows.len >= 0 // At least the default session should exist
tmux.stop()! tmux.stop()!
} }
fn test_scan() ! { fn test_scan() ! {
console.print_debug('-----Testing scan------') console.print_debug('-----Testing scan------')
mut tmux := new()! mut tmux := new()!
tmux.start()! tmux.start()!
// Test initial scan // Test initial scan
tmux.scan()! tmux.scan()!
sessions_before := tmux.sessions.len sessions_before := tmux.sessions.len
// Create a test session // Create a test session
mut session := tmux.session_create(name: 'test_scan')! mut session := tmux.session_create(name: 'test_scan')!
// Scan again // Scan again
tmux.scan()! tmux.scan()!
sessions_after := tmux.sessions.len sessions_after := tmux.sessions.len
assert sessions_after >= sessions_before assert sessions_after >= sessions_before
tmux.stop()! tmux.stop()!
} }
// //TODO: fix test // //TODO: fix test

View File

@@ -13,7 +13,7 @@ pub mut:
session &Session @[skip] session &Session @[skip]
name string name string
id int id int
panes []&Pane // windows contain multiple panes panes []&Pane // windows contain multiple panes
active bool active bool
env map[string]string env map[string]string
} }
@@ -22,105 +22,104 @@ pub mut:
pub struct PaneNewArgs { pub struct PaneNewArgs {
pub mut: pub mut:
name string name string
reset bool //means we reset the pane if it already exists reset bool // means we reset the pane if it already exists
cmd string cmd string
env map[string]string env map[string]string
} }
pub fn (mut w Window) scan() ! { pub fn (mut w Window) scan() ! {
// Get current panes for this window // Get current panes for this window
cmd := "tmux list-panes -t ${w.session.name}:@${w.id} -F '#{pane_id}|#{pane_pid}|#{pane_active}|#{pane_start_command}'" cmd := "tmux list-panes -t ${w.session.name}:@${w.id} -F '#{pane_id}|#{pane_pid}|#{pane_active}|#{pane_start_command}'"
result := osal.execute_silent(cmd) or { result := osal.execute_silent(cmd) or {
// Window might not exist anymore // Window might not exist anymore
return return
} }
mut current_panes := map[int]bool{} mut current_panes := map[int]bool{}
for line in result.split_into_lines() { for line in result.split_into_lines() {
if line.contains('|') { if line.contains('|') {
parts := line.split('|') parts := line.split('|')
if parts.len >= 3 { if parts.len >= 3 {
pane_id := parts[0].replace('%', '').int() pane_id := parts[0].replace('%', '').int()
pane_pid := parts[1].int() pane_pid := parts[1].int()
pane_active := parts[2] == '1' pane_active := parts[2] == '1'
pane_cmd := if parts.len > 3 { parts[3] } else { '' } pane_cmd := if parts.len > 3 { parts[3] } else { '' }
current_panes[pane_id] = true current_panes[pane_id] = true
// Update existing pane or create new one // Update existing pane or create new one
mut found := false mut found := false
for mut p in w.panes { for mut p in w.panes {
if p.id == pane_id { if p.id == pane_id {
p.pid = pane_pid p.pid = pane_pid
p.active = pane_active p.active = pane_active
p.cmd = pane_cmd p.cmd = pane_cmd
found = true found = true
break break
} }
} }
if !found { if !found {
mut new_pane := Pane{ mut new_pane := Pane{
window: &w window: &w
id: pane_id id: pane_id
pid: pane_pid pid: pane_pid
active: pane_active active: pane_active
cmd: pane_cmd cmd: pane_cmd
env: map[string]string{} env: map[string]string{}
created_at: time.now() created_at: time.now()
last_output_offset: 0 last_output_offset: 0
} }
w.panes << &new_pane w.panes << &new_pane
} }
} }
} }
} }
// Remove panes that no longer exist // Remove panes that no longer exist
w.panes = w.panes.filter(current_panes[it.id] == true) w.panes = w.panes.filter(current_panes[it.id] == true)
} }
pub fn (mut w Window) stop() ! { pub fn (mut w Window) stop() ! {
w.kill()! w.kill()!
} }
//helper function
//TODO env variables are not inserted in pane // helper function
// TODO env variables are not inserted in pane
pub fn (mut w Window) create(cmd_ string) ! { pub fn (mut w Window) create(cmd_ string) ! {
mut final_cmd := cmd_ mut final_cmd := cmd_
if cmd_.contains('\n') { if cmd_.contains('\n') {
os.mkdir_all('/tmp/tmux/${w.session.name}')! os.mkdir_all('/tmp/tmux/${w.session.name}')!
// Fix: osal.exec_string doesn't exist, use file writing instead // Fix: osal.exec_string doesn't exist, use file writing instead
script_path := '/tmp/tmux/${w.session.name}/${w.name}.sh' script_path := '/tmp/tmux/${w.session.name}/${w.name}.sh'
script_content := '#!/bin/bash\n' + cmd_ script_content := '#!/bin/bash\n' + cmd_
os.write_file(script_path, script_content)! os.write_file(script_path, script_content)!
os.chmod(script_path, 0o755)! os.chmod(script_path, 0o755)!
final_cmd = script_path final_cmd = script_path
} }
mut newcmd := '/bin/bash -c "${final_cmd}"' mut newcmd := '/bin/bash -c "${final_cmd}"'
if cmd_ == "" { if cmd_ == '' {
newcmd = '/bin/bash' newcmd = '/bin/bash'
} }
// Build environment arguments // Build environment arguments
mut env_args := '' mut env_args := ''
for key, value in w.env { for key, value in w.env {
env_args += ' -e ${key}="${value}"' env_args += ' -e ${key}="${value}"'
} }
res_opt := "-P -F '#{session_name}|#{window_name}|#{window_id}|#{pane_active}|#{pane_id}|#{pane_pid}|#{pane_start_command}'" res_opt := "-P -F '#{session_name}|#{window_name}|#{window_id}|#{pane_active}|#{pane_id}|#{pane_pid}|#{pane_start_command}'"
cmd := 'tmux new-window ${res_opt}${env_args} -t ${w.session.name} -n ${w.name} \'${newcmd}\'' cmd := 'tmux new-window ${res_opt}${env_args} -t ${w.session.name} -n ${w.name} \'${newcmd}\''
console.print_debug(cmd) console.print_debug(cmd)
res := osal.exec(cmd: cmd, stdout: false, name: 'tmux_window_create') or { res := osal.exec(cmd: cmd, stdout: false, name: 'tmux_window_create') or {
return error("Can't create new window ${w.name} \n${cmd}\n${err}") return error("Can't create new window ${w.name} \n${cmd}\n${err}")
} }
line_arr := res.output.split('|') line_arr := res.output.split('|')
wid := line_arr[2] or { return error('cannot split line for window create.\n${line_arr}') } wid := line_arr[2] or { return error('cannot split line for window create.\n${line_arr}') }
w.id = wid.replace('@', '').int() w.id = wid.replace('@', '').int()
} }
// stop the window // stop the window
@@ -143,14 +142,14 @@ pub fn (window Window) str() string {
} }
pub fn (mut w Window) stats() !ProcessStats { pub fn (mut w Window) stats() !ProcessStats {
mut total := ProcessStats{} mut total := ProcessStats{}
for mut pane in w.panes { for mut pane in w.panes {
stats := pane.stats() or { continue } stats := pane.stats() or { continue }
total.cpu_percent += stats.cpu_percent total.cpu_percent += stats.cpu_percent
total.memory_bytes += stats.memory_bytes total.memory_bytes += stats.memory_bytes
total.memory_percent += stats.memory_percent total.memory_percent += stats.memory_percent
} }
return total return total
} }
// will select the current window so with tmux a we can go there . // will select the current window so with tmux a we can go there .
@@ -169,10 +168,10 @@ pub fn (mut w Window) pane_list() []&Pane {
// Get active pane in window // Get active pane in window
pub fn (mut w Window) pane_active() ?&Pane { pub fn (mut w Window) pane_active() ?&Pane {
for pane in w.panes { for pane in w.panes {
if pane.active { if pane.active {
return pane return pane
} }
} }
return none return none
} }

View File

@@ -24,23 +24,23 @@ fn testsuite_end() {
} }
fn test_window_new() ! { fn test_window_new() ! {
mut tmux := new()! mut tmux := new()!
tmux.start()! tmux.start()!
// Create session first // Create session first
mut session := tmux.session_create(name: 'main')! mut session := tmux.session_create(name: 'main')!
// Test window creation // Test window creation
mut window := session.window_new( mut window := session.window_new(
name: 'TestWindow' name: 'TestWindow'
cmd: 'bash' cmd: 'bash'
reset: true reset: true
)! )!
assert window.name == 'testwindow' // name_fix converts to lowercase assert window.name == 'testwindow' // name_fix converts to lowercase
assert session.window_exist(name: 'testwindow') assert session.window_exist(name: 'testwindow')
tmux.stop()! tmux.stop()!
} }
// tests creating duplicate windows // tests creating duplicate windows

View File

@@ -1,18 +1,18 @@
module datamodel module datamodel
// I can bid for infra, and optionally get accepted
@[heap] @[heap]
//I can bid for infra, and optionally get accepted
pub struct Bid { pub struct Bid {
pub mut: pub mut:
id u32 id u32
customer_id u32 //links back to customer for this capacity (user on ledger) customer_id u32 // links back to customer for this capacity (user on ledger)
compute_slices_nr int //nr of slices I need in 1 machine compute_slices_nr int // nr of slices I need in 1 machine
compute_slice f64 //price per 1 GB slice I want to accept compute_slice f64 // price per 1 GB slice I want to accept
storage_slices []u32 storage_slices []u32
status BidStatus status BidStatus
obligation bool //if obligation then will be charged and money needs to be in escrow, otherwise its an intent obligation bool // if obligation then will be charged and money needs to be in escrow, otherwise its an intent
start_date u32 //epoch start_date u32 // epoch
end_date u32 end_date u32
} }
pub enum BidStatus { pub enum BidStatus {

View File

@@ -3,13 +3,13 @@ module datamodel
@[heap] @[heap]
pub struct Reservation { pub struct Reservation {
pub mut: pub mut:
id u32 id u32
customer_id u32 //links back to customer for this capacity customer_id u32 // links back to customer for this capacity
compute_slices []u32 compute_slices []u32
storage_slices []u32 storage_slices []u32
status ReservationStatus status ReservationStatus
start_date u32 //epoch start_date u32 // epoch
end_date u32 end_date u32
} }
pub enum ReservationStatus { pub enum ReservationStatus {

View File

@@ -1,5 +1,7 @@
module datamodel module datamodel
import freeflowuniverse.herolib.threefold.grid4.datamodel { Node } import freeflowuniverse.herolib.threefold.grid4.datamodel { Node }
pub struct NodeSim { pub struct NodeSim {
Node Node
pub mut: pub mut: