From dc062c75c92a9b5d15bc5e85ece5ec614f904fe3 Mon Sep 17 00:00:00 2001 From: Ilya Verbitskiy Date: Sat, 27 Jan 2024 00:24:25 +0100 Subject: [PATCH] virtuerl: init wxwidgets ui --- virtuerl/src/virtuerl_ipam.erl | 5 + virtuerl/src/virtuerl_mgt.erl | 32 +-- virtuerl/src/virtuerl_net.erl | 16 +- virtuerl/src/virtuerl_pubsub.erl | 52 +++++ virtuerl/src/virtuerl_qemu.erl | 88 +++++---- virtuerl/src/virtuerl_sup.erl | 10 +- virtuerl/src/virtuerl_ui.erl | 330 +++++++++++++++++++++++++++++++ virtuerl/src/virtuerl_vnc.erl | 239 ++++++++++++++++++++++ 8 files changed, 724 insertions(+), 48 deletions(-) create mode 100644 virtuerl/src/virtuerl_pubsub.erl create mode 100644 virtuerl/src/virtuerl_ui.erl create mode 100644 virtuerl/src/virtuerl_vnc.erl diff --git a/virtuerl/src/virtuerl_ipam.erl b/virtuerl/src/virtuerl_ipam.erl index a839199..3eb23a4 100644 --- a/virtuerl/src/virtuerl_ipam.erl +++ b/virtuerl/src/virtuerl_ipam.erl @@ -47,6 +47,7 @@ ipam_put_net(NetworkDef) -> Other end. +-spec ipam_list_nets() -> {ok, #{binary() => #{cidr4 | cidr6 := #{address := nonempty_binary(), prefixlen := integer()}}}}. ipam_list_nets() -> gen_server:call(ipam, net_list). @@ -197,6 +198,10 @@ handle_call({ip_put, NetworkId, IpAddr, DomainId}, _From, StoreId) -> end, {reply, R, StoreId}; +handle_call({ip_delete, NetworkId, IpAddr}, _From, StoreId) -> + R = khepri:delete(StoreId, [network, NetworkId, ?KHEPRI_WILDCARD_STAR, IpAddr]), + {reply, R, StoreId}; + handle_call({ip_clear}, _From, StoreId) -> R = khepri:delete(StoreId, [network]), {reply, R, StoreId}. diff --git a/virtuerl/src/virtuerl_mgt.erl b/virtuerl/src/virtuerl_mgt.erl index 8aac529..b52c921 100644 --- a/virtuerl/src/virtuerl_mgt.erl +++ b/virtuerl/src/virtuerl_mgt.erl @@ -11,7 +11,7 @@ -export([start_link/0]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3, handle_continue/2]). --export([create_vm/0, domain_create/1, domain_get/1, domain_delete/1, domain_stop/1, domain_start/1]). +-export([create_vm/0, domain_create/1, domain_get/1, domain_delete/1, domain_stop/1, domain_start/1, domains_list/0]). -export([home_path/0]). -define(SERVER, ?MODULE). @@ -30,8 +30,9 @@ domain_delete(Conf) -> domain_get(Conf) -> gen_server:call(?SERVER, {domain_get, Conf}). -domains_list(Conf) -> - gen_server:call(?SERVER, {domains_list, Conf}). +-spec domains_list() -> #{}. +domains_list() -> + gen_server:call(?SERVER, domains_list). domain_stop(Id) -> gen_server:call(?SERVER, {domain_update, #{id => Id, state => stopped}}). @@ -60,7 +61,8 @@ init([]) -> handle_continue(setup_base, State) -> ok = filelib:ensure_path(filename:join(home_path(), "domains")), - BaseImagePath = filename:join(home_path(), "debian-12-genericcloud-amd64-20230910-1499.qcow2"), +%% BaseImagePath = filename:join(home_path(), "debian-12-genericcloud-amd64-20230910-1499.qcow2"), + BaseImagePath = filename:join(home_path(), "openSUSE-Leap-15.5.x86_64-NoCloud.qcow2"), case filelib:is_regular(BaseImagePath) of true -> ok; false -> @@ -108,12 +110,12 @@ generate_unique_tap_name(TapNames) -> handle_call({domain_create, Conf}, _From, State) -> {Table} = State, - #{network_id := NetworkID} = Conf, DomainID = virtuerl_util:uuid4(), - Domain = #{id => DomainID, network_id => NetworkID}, % TODO: save ipv4 addr as well + Domain = maps:merge(#{id => DomainID, name => DomainID}, Conf), % TODO: save ipv4 addr as well dets:insert_new(Table, {DomainID, Domain}), dets:sync(Table), + #{network_id := NetworkID} = Domain, {ok, #{cidrs := Cidrs}} = virtuerl_ipam:ipam_get_net(NetworkID), Keys = lists:map(fun(Cidr) -> {Ip, _} = virtuerl_net:parse_cidr(Cidr), @@ -151,8 +153,8 @@ handle_call({domain_create, Conf}, _From, State) -> Domains = dets:match_object(Table, '_'), TapNames = sets:from_list([Tap || #{tap_name := Tap} <- Domains]), TapName = generate_unique_tap_name(TapNames), - <> = <<(rand:uniform(16#ffffffffffff)):48>>, - MacAddr = <>, + <> = <<(rand:uniform(16#ffffffffffff)):48>>, + MacAddr = <>, dets:insert(Table, {DomainID, Domain#{network_addrs => Cidrs, mac_addr=>MacAddr, ipv4_addr=>Ipv4Addr, ipv6_addr => Ipv6Addr, cidrs => IpCidrs, tap_name => TapName}}), dets:sync(Table), @@ -171,18 +173,22 @@ handle_call({domain_create, Conf}, _From, State) -> handle_call({domain_update, #{id := DomainID, state := RunState}}, _From, {Table} = State) -> Reply = case dets:lookup(Table, DomainID) of [{_, Domain}] -> - ok = dets:insert(Table, {DomainID, Domain#{state := RunState}}), + ok = dets:insert(Table, {DomainID, Domain#{state => RunState}}), ok = dets:sync(Table), ok; [] -> notfound end, {reply, Reply, State, {continue, sync_domains}}; +handle_call(domains_list, _From, State) -> + {Table} = State, + Domains = dets:match_object(Table, '_'), + {reply, [maps:merge(#{state => stopped, name => Id}, Domain) || {Id, Domain} <- Domains], State}; handle_call({domain_get, #{id := DomainID}}, _From, State) -> {Table} = State, Reply = case dets:lookup(Table, DomainID) of - [{_, #{network_id := NetworkID, mac_addr := MacAddr, ipv4_addr:=IP, tap_name := TapName}}] -> - DomRet = #{network_id => NetworkID, mac_addr => binary:encode_hex(MacAddr), ipv4_addr => virtuerl_net:format_ip_bitstring(IP), tap_name => iolist_to_binary(TapName)}, - {ok, DomRet}; + [{_, #{mac_addr := MacAddr, ipv4_addr:=IP, tap_name := TapName} = Domain}] -> + DomRet = Domain#{mac_addr := binary:encode_hex(MacAddr), ipv4_addr := virtuerl_net:format_ip_bitstring(IP), tap_name := iolist_to_binary(TapName)}, + {ok, maps:merge(#{name => DomainID}, DomRet)}; [] -> notfound end, {reply, Reply, State}; @@ -190,7 +196,9 @@ handle_call({domain_delete, #{id := DomainID}}, _From, State) -> {Table} = State, Res = dets:delete(Table, DomainID), dets:sync(Table), + io:format("terminating ~p~n", [DomainID]), ok = supervisor:terminate_child(virtuerl_sup, DomainID), + io:format("done terminating ~p~n", [DomainID]), ok = supervisor:delete_child(virtuerl_sup, DomainID), ok = gen_server:call(virtuerl_net, {net_update}), {reply, Res, State}. diff --git a/virtuerl/src/virtuerl_net.erl b/virtuerl/src/virtuerl_net.erl index 3a60067..01d9a5d 100644 --- a/virtuerl/src/virtuerl_net.erl +++ b/virtuerl/src/virtuerl_net.erl @@ -139,6 +139,11 @@ update_nftables(Domains) -> " type nat hook prerouting priority dstnat - 5; policy accept;\n", DnsRules, " }\n", + + " chain postrouting {\n", + " type nat hook postrouting priority -5; policy accept;\n", + " oifname != \"verlbr*\" iifname \"verlbr*\" masquerade\n", + " }\n", "}\n" ], io:format("~s~n", [IoList]), @@ -147,7 +152,11 @@ update_nftables(Domains) -> ok = filelib:ensure_dir(Path), ok = file:write_file(Path, IoList), - os:cmd(io_lib:format("nft -f ~s", [Path])), + NftOut = os:cmd(io_lib:format("nft -f ~s", [Path])), + case NftOut of + "" -> ok; + _ -> error({nft_error, NftOut}) + end, file:delete(Path). network_cidrs_to_bride_cidrs(Cidrs) -> @@ -249,6 +258,9 @@ generate_unique_bridge_name(Ifnames) -> generate_unique_bridge_name(Ifnames) end. +to_vtap_map(MacAddr) -> + <> = MacAddr, + <>. sync_taps(Domains) -> Output = os:cmd("ip -j addr"), @@ -262,7 +274,7 @@ sync_taps(Domains) -> TapsTarget = sets:from_list([iolist_to_binary(TapName) || {_, #{tap_name := TapName}} <- Domains]), % TODO: persist tap_name as binary io:format("Taps Target: ~p~n", [sets:to_list(TapsTarget)]), io:format("Taps Actual: ~p~n", [sets:to_list(TapsActual)]), - TapsMap = maps:from_list([{iolist_to_binary(Tap), {MacAddr, network_cidrs_to_bride_cidrs(Cidrs)}} || {_, #{network_addrs := Cidrs, tap_name := Tap, mac_addr := MacAddr}} <- Domains]), + TapsMap = maps:from_list([{iolist_to_binary(Tap), {to_vtap_map(MacAddr), network_cidrs_to_bride_cidrs(Cidrs)}} || {_, #{network_addrs := Cidrs, tap_name := Tap, mac_addr := MacAddr}} <- Domains]), io:format("TapsMap: ~p~n", [TapsMap]), TapsToDelete = sets:subtract(TapsActual, TapsTarget), diff --git a/virtuerl/src/virtuerl_pubsub.erl b/virtuerl/src/virtuerl_pubsub.erl new file mode 100644 index 0000000..1eaf844 --- /dev/null +++ b/virtuerl/src/virtuerl_pubsub.erl @@ -0,0 +1,52 @@ +%%%------------------------------------------------------------------- +%%% @author ilya +%%% @copyright (C) 2023, +%%% @doc +%%% @end +%%%------------------------------------------------------------------- +-module(virtuerl_pubsub). + +-behaviour(gen_server). + +-export([start_link/0]). +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, + code_change/3]). +-export([subscribe/0, send/1]). + +-define(SERVER, ?MODULE). +-define(APPLICATION, virtuerl). + +%%%=================================================================== +%%% Spawning and gen_server implementation +%%%=================================================================== + +subscribe() -> + gen_server:call(?SERVER, subscribe). + +send(Message) -> + gen_server:cast(?SERVER, {send, Message}). + +start_link() -> + gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). + +init([]) -> + {ok, []}. + +handle_call(subscribe, {Pid, _Tag}, State) -> + _Ref = monitor(process, Pid), + {reply, ok, [Pid | State]}. + +handle_cast({send, Message}, State) -> + [Pid ! Message || Pid <- State], + {noreply, State}. + +handle_info({'DOWN', Ref, process, Pid, Reason}, State) -> + io:format("~p died because of ~p~n", [Pid, Reason]), + NewState = lists:delete(Pid, State), + {noreply, NewState}. + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. diff --git a/virtuerl/src/virtuerl_qemu.erl b/virtuerl/src/virtuerl_qemu.erl index 54def68..9ce0f7f 100644 --- a/virtuerl/src/virtuerl_qemu.erl +++ b/virtuerl/src/virtuerl_qemu.erl @@ -38,21 +38,23 @@ callback_mode() -> init([ID]) -> {ok, Table} = dets:open_file(domains, [{file, filename:join(virtuerl_mgt:home_path(), "domains.dets")}]), - [{DomainId, #{mac_addr:=MacAddr, tap_name := TapName} = Domain}] = dets:lookup(Table, ID), + [{DomainId, #{mac_addr:=MacAddr, tap_name := TapName} = DomainRaw}] = dets:lookup(Table, ID), + Domain = maps:merge(#{user_data => ""}, DomainRaw), DomainHomePath = filename:join([virtuerl_mgt:home_path(), "domains", DomainId]), ok = filelib:ensure_path(DomainHomePath), RootVolumePath = filename:join(DomainHomePath, "root.qcow2"), case filelib:is_regular(RootVolumePath) of false -> - BaseImagePath = filename:join([virtuerl_mgt:home_path(), "debian-12-genericcloud-amd64-20230910-1499.qcow2"]), - exec:run(io_lib:format("qemu-img create -f qcow2 -b ~s -F qcow2 ~s", [filename:absname(BaseImagePath), RootVolumePath])); +%% BaseImagePath = filename:join([virtuerl_mgt:home_path(), "debian-12-genericcloud-amd64-20230910-1499.qcow2"]), + BaseImagePath = filename:join([virtuerl_mgt:home_path(), "openSUSE-Leap-15.5.x86_64-NoCloud.qcow2"]), + exec:run(lists:flatten(io_lib:format("qemu-img create -f qcow2 -b ~s -F qcow2 ~s", [filename:absname(BaseImagePath), RootVolumePath])), [sync]); _ -> noop end, process_flag(trap_exit, true), ensure_cloud_config(Domain), file:delete(filename:join(DomainHomePath, "qmp.sock")), file:delete(filename:join(DomainHomePath, "serial.sock")), - Cmd = iolist_to_binary(["kvm -no-shutdown -S -nic tap,ifname=",TapName,",script=no,downscript=no,model=virtio-net-pci,mac=",virtuerl_util:mac_to_str(MacAddr), " -display none -m 512 -drive file=root.qcow2,if=virtio -drive driver=raw,file=cloud_config.iso,if=virtio -display none -qmp unix:qmp.sock,server=on,wait=off -serial unix:serial.sock,server=on,wait=off"]), + Cmd = iolist_to_binary(["kvm -no-shutdown -S -nic tap,ifname=",TapName,",script=no,downscript=no,model=virtio-net-pci,mac=",virtuerl_util:mac_to_str(MacAddr), " -vnc :1 -display none -serial none -m 512 -drive file=root.qcow2,if=virtio -drive driver=raw,file=cloud_config.iso,if=virtio -qmp unix:qmp.sock,server=on,wait=off"]), % -serial unix:serial.sock,server=on,wait=off io:format("QEMU cmdline: ~s~n", [Cmd]), {ok, Pid, OsPid} = exec:run_link(Cmd, [{cd, DomainHomePath}]), State = #{table => Table, id => ID, domain => Domain, qemu_pid => {Pid, OsPid}, qmp_pid => undefined}, @@ -62,11 +64,27 @@ handle_continue(setup_qmp, #{id := ID} = State) -> QmpSocketPath = filename:join([virtuerl_mgt:home_path(), "domains", ID, "qmp.sock"]), io:format("waiting for qmp.sock ~p~n", [erlang:timestamp()]), %% {ok, _} = exec:run(iolist_to_binary(["inotifywait -e create --include 'qmp\\.sock' ", ID]), [sync]), - ok = wait_for_socket(ID), - io:format("done waiting for qmp.sock ~p~n", [erlang:timestamp()]), - {ok, QmpPid} = virtuerl_qmp:start_link(QmpSocketPath, self()), - virtuerl_qmp:exec(QmpPid, cont), - {noreply, State#{qmp_pid => QmpPid}}. + case wait_for_socket(QmpSocketPath) of + ok -> + {ok, QmpPid} = virtuerl_qmp:start_link(QmpSocketPath, self()), + virtuerl_qmp:exec(QmpPid, cont), + {noreply, State#{qmp_pid => QmpPid}, {continue, setup_serial}}; + timeout -> + {stop, failure, State} + end; +handle_continue(setup_serial, #{id := ID} = State) -> + {noreply, State}. +%% SerialSocketPath = filename:join([virtuerl_mgt:home_path(), "domains", ID, "serial.sock"]), +%% io:format("waiting for serial.sock ~p~n", [erlang:timestamp()]), +%%%% {ok, _} = exec:run(iolist_to_binary(["inotifywait -e create --include 'qmp\\.sock' ", ID]), [sync]), +%% case wait_for_socket(SerialSocketPath) of +%% ok -> +%% % TODO: shall this be its own process instead? +%% {ok, SerialSocket} = gen_tcp:connect({local, SerialSocketPath}, 0, [local, {active, true}, {packet, line}, binary]), +%% {noreply, State#{serial_socket => SerialSocket}}; +%% timeout -> +%% {stop, failure, State} +%% end. handle_info({qmp, Event}, #{table := Table, id := ID, domain := Domain} = State) -> io:format("QMP: ~p~n", [Event]), @@ -78,7 +96,10 @@ handle_info({qmp, Event}, #{table := Table, id := ID, domain := Domain} = State) ok = dets:sync(Table), {stop, normal, State#{domain => DomainUpdated}}; _ -> {noreply, State} - end. + end; +handle_info({tcp, SerialSocket, Data}, #{table := Table, id := ID, domain := Domain, serial_socket := SerialSocket} = State) -> + virtuerl_pubsub:send({domain_out, ID, Data}), + {noreply, State}. shutdown_events() -> receive @@ -127,14 +148,29 @@ terminate(_Reason, #{table := Table, id := ID, domain := Domain, qemu_pid := {Pi %%% Internal functions %%%=================================================================== -wait_for_socket(ID) -> - QmpSocketPath = filename:join([virtuerl_mgt:home_path(), "domains", ID, "qmp.sock"]), +wait_for_socket(SocketPath) -> + Self = self(), + WaiterPid = spawn(fun () -> + do_wait_for_socket(SocketPath, Self) + end), + receive + {virtuerl, socket_available} -> + io:format("done waiting for ~s ~p~n", [SocketPath, erlang:timestamp()]), + ok + after 2000 -> + io:format("failed waiting"), + exit(WaiterPid, kill), + timeout + end. + +do_wait_for_socket(SocketPath, Requester) -> io:format("checking...~n"), - case filelib:last_modified(QmpSocketPath) of + case filelib:last_modified(SocketPath) of 0 -> timer:sleep(20), - wait_for_socket(ID); - _ -> ok + do_wait_for_socket(SocketPath, Requester); + _ -> + Requester ! {virtuerl, socket_available} end. ensure_cloud_config(#{id := DomainID} = Domain) -> @@ -143,7 +179,7 @@ ensure_cloud_config(#{id := DomainID} = Domain) -> false -> create_cloud_config(Domain) end. -create_cloud_config(#{id := DomainID, mac_addr := MacAddr, cidrs := Cidrs}) -> +create_cloud_config(#{id := DomainID, mac_addr := MacAddr, cidrs := Cidrs, user_data := UserData}) -> NetConf = [ "version: 2\n", "ethernets:\n", @@ -154,30 +190,16 @@ create_cloud_config(#{id := DomainID, mac_addr := MacAddr, cidrs := Cidrs}) -> " dhcp4: false\n", " dhcp6: false\n", " addresses:\n", [[ - " - ", virtuerl_net:format_ip(IpAddr), "/", integer_to_binary(Prefixlen), "\n"] || {IpAddr, Prefixlen} <- Cidrs], + " - ", virtuerl_net:format_ip(IpAddr), "/", integer_to_binary(Prefixlen), "\n"] || {IpAddr, Prefixlen} <- Cidrs], " routes:\n", [[ - " - to: default\n", - " via: ", virtuerl_net:format_ip(virtuerl_net:bridge_addr(IpAddr, Prefixlen)), "\n"] || {IpAddr, Prefixlen} <- Cidrs], + " - to: default\n", + " via: ", virtuerl_net:format_ip(virtuerl_net:bridge_addr(IpAddr, Prefixlen)), "\n"] || {IpAddr, Prefixlen} <- Cidrs], "" ], MetaData = [ "instance-id: ", DomainID, "\n", "local-hostname: ", DomainID, "\n" ], - % openssl passwd -6 - UserData = [ - "#cloud-config\n", - "users:\n", - " - default\n", - " - name: ilya\n", - " passwd: $6$VAKAlO91OquUA1ON$4w2.omQG9OUt0KuMtCvYrJgervyK8WZrTMFUhJI2fXTsfMN5YvuxKaZbSJTXkJvq4qAiFHptt6zKg2hTMOrkH0\n", - " lock_passwd: false\n", - " shell: /bin/bash\n", - " sudo: ALL=(ALL) NOPASSWD:ALL\n", - " ssh_authorized_keys:\n", - " - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDCKxHNpVg1whPegPv0KcRQTOfyVIqLwvMfVLyT9OpBPXHDudsFz9soOgMUEyWm8ZJ+pJ9fRCg66B+D5/ZRTwJCBpyNncfXCwu8xEJgEeoIubObh6t6dHWqqxX/yhHAS5GIRUSypm78qg6V+SQ6SeJXSjOCLAbZmhyWgJrlDm9M6GTPQhPAztrgsCUrzxIpZ5el5BwJXrm3I+LOmofAUqgbLQz9HuGJzPpnfABDa9WoVfI0L7oTr0qGpWwx8l71b2s8AYl7GMD/bEkZKyi9SSwEVCHA88F7dYYrZ3+fMXE/mJf+v0ece2lIDT7Te1gtqiLu/izJNmqD+b6mtnnXxVxNOtynhv3t6uLE9kBX22SBCCRqPJzETGNXvYH6fATEe88dhLh8kTppLRB5UGUd/zztxuNBSpMwFXaq8SlTKURxvF8BuFIPCz0FW8fq+TA/xZfBYsiVt59jXgl6BQyEGY4bMuMtT2nD8QXwZ5vsj52mzKGJwBwduiaX302brHYUyQkuyLII5iqmCNZ5YLlMY76a61Yg9pWMeRwQscSO2k4a18GOo+sIrQVTyUQiT3KhRRaDNrZuCPicQRgkJuiS1fKt1cWjnOlyweLxSYbpKnoS0H7vt+NrtbU1u9FPknXQPQ0pxixPpV3zgUdfOLmisFH7WGVjwNVvZAlNc5uyqm0fbw== ilya@verbit.io\n", - "" - ], DomainBasePath = filename:join([virtuerl_mgt:home_path(), "domains", DomainID]), IsoBasePath = filename:join(DomainBasePath, "iso"), ok = filelib:ensure_path(IsoBasePath), diff --git a/virtuerl/src/virtuerl_sup.erl b/virtuerl/src/virtuerl_sup.erl index fec95ef..72f6acb 100644 --- a/virtuerl/src/virtuerl_sup.erl +++ b/virtuerl/src/virtuerl_sup.erl @@ -29,7 +29,15 @@ init([]) -> SupFlags = #{strategy => one_for_one, intensity => 300, period => 5}, - ChildSpecs = [{ + ChildSpecs = [ + { + virtuerl_pubsub, + {virtuerl_pubsub, start_link, []}, + permanent, + infinity, + worker, + [] + }, { virtuerl_ipam, {virtuerl_ipam, start_link, []}, permanent, diff --git a/virtuerl/src/virtuerl_ui.erl b/virtuerl/src/virtuerl_ui.erl new file mode 100644 index 0000000..ca2238f --- /dev/null +++ b/virtuerl/src/virtuerl_ui.erl @@ -0,0 +1,330 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2009-2021. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%% +%% %CopyrightEnd% +%% +%%%------------------------------------------------------------------- +%%% File : hello.erl +%%% Author : Matthew Harrison +%%% Description : _really_ minimal example of a wxerlang app +%%% implemented with wx_object behaviour +%%% +%%% Created : 18 Sep 2008 by Matthew Harrison +%%% Dan rewrote it to show wx_object behaviour +%%%------------------------------------------------------------------- +-module(virtuerl_ui). +-include_lib("wx/include/wx.hrl"). + +-export([start/0, + init/1, handle_info/2, handle_event/2, handle_call/3, + code_change/3, terminate/2]). + +-behaviour(wx_object). + +-record(state, {win, info_panel, info, domain_panel, domain_info, toolbar, domain_list_box, domains}). + +start() -> + wx_object:start_link(?MODULE, [], []). + +%% Init is called in the new process. +init([]) -> + virtuerl_pubsub:subscribe(), + + wx:new(), + Frame = wxFrame:new(wx:null(), + -1, % window id + "Hello World", % window title + []), + + MenuBar = wxMenuBar:new(), + Menu = wxMenu:new(), + wxMenu:append(Menu, ?wxID_EXIT, "Quit"), + wxMenuBar:append(MenuBar, Menu, "File"), + wxFrame:setMenuBar(Frame, MenuBar), + wxFrame:connect(Frame, command_menu_selected), + + {Mx, My, _, _} = wxMiniFrame:getTextExtent(Frame, "M"), + wxFrame:setClientSize(Frame, {60*Mx, 20*My}), + + PlayIconDC = wxMemoryDC:new(), + PlayIcon = wxBitmap:new(30, 30, [{depth, 32}]), + wxBufferedDC:selectObject(PlayIconDC, PlayIcon), + wxMemoryDC:setBrush(PlayIconDC, ?wxGREEN_BRUSH), + wxMemoryDC:setPen(PlayIconDC, ?wxGREEN_PEN), + wxMemoryDC:drawPolygon(PlayIconDC, [{0,0},{30,15},{0,30}]), + wxMemoryDC:destroy(PlayIconDC), + PlayIconDisabled = wxBitmap:new(wxImage:convertToGreyscale(wxBitmap:convertToImage(PlayIcon))), + + StopIconDC = wxMemoryDC:new(), + StopIcon = wxBitmap:new(30, 30, [{depth, 32}]), + wxBufferedDC:selectObject(StopIconDC, StopIcon), + wxMemoryDC:setBrush(StopIconDC, ?wxRED_BRUSH), + wxMemoryDC:setPen(StopIconDC, ?wxRED_PEN), + wxMemoryDC:drawRectangle(StopIconDC, {0,0},{30,30}), + wxMemoryDC:destroy(StopIconDC), + StopIconDisabled = wxBitmap:new(wxImage:convertToGreyscale(wxBitmap:convertToImage(StopIcon))), + + Toolbar = wxFrame:createToolBar(Frame), + wxToolBar:addTool(Toolbar, 100, "test123", PlayIcon, PlayIconDisabled), + wxToolBar:enableTool(Toolbar, 100, false), + wxToolBar:addTool(Toolbar, 101, "test123", StopIcon, StopIconDisabled), + wxToolBar:enableTool(Toolbar, 101, false), + wxToolBar:realize(Toolbar), + wxToolBar:connect(Toolbar, command_menu_selected), + + wxFrame:createStatusBar(Frame,[]), + + %% if we don't handle this ourselves, wxwidgets will close the window + %% when the user clicks the frame's close button, but the event loop still runs + wxFrame:connect(Frame, close_window), + + RootPanel = wxPanel:new(Frame, [{size, wxFrame:getSize(Frame)}]), + Notebook = wxNotebook:new(RootPanel, ?wxID_ANY), + + Sizer = wxBoxSizer:new(?wxHORIZONTAL), + wxSizer:add(Sizer, Notebook, [{flag, ?wxEXPAND}, {proportion, 1}]), + wxPanel:setSizer(RootPanel, Sizer), + + + NetworkPanel = wxPanel:new(Notebook, []), + NetworksSizer = wxBoxSizer:new(?wxHORIZONTAL), + NetworkSplitter = wxSplitterWindow:new(NetworkPanel), + wxSizer:add(NetworksSizer, NetworkSplitter, [{flag, ?wxEXPAND}, {proportion, 1}]), + wxPanel:setSizer(NetworkPanel, NetworksSizer), + + wxNotebook:addPage(Notebook, NetworkPanel, "Networks"), + + {ok, Nets} = virtuerl_ipam:ipam_list_nets(), + Choices = maps:keys(Nets), + ListBox = wxListBox:new(NetworkSplitter, 42, [{choices, Choices}]), + wxListBox:connect(ListBox, command_listbox_selected), % command_listbox_doubleclicked + + Info = wxPanel:new(NetworkSplitter), + InfoSizer = wxBoxSizer:new(?wxHORIZONTAL), + InfoGrid = wxFlexGridSizer:new(3, 2, 0, 0), + wxSizer:add(InfoSizer, InfoGrid, [{flag, ?wxEXPAND}, {proportion, 1}]), + wxPanel:setSizer(Info, InfoSizer), + + wxSplitterWindow:splitVertically(NetworkSplitter, ListBox, Info, [{sashPosition, 25 * Mx}]), + + % BEGIN Domains + DomainPanel = wxPanel:new(Notebook, []), + DomainsSizer = wxBoxSizer:new(?wxHORIZONTAL), + DomainSplitter = wxSplitterWindow:new(DomainPanel), + wxSizer:add(DomainsSizer, DomainSplitter, [{flag, ?wxEXPAND}, {proportion, 1}]), + wxPanel:setSizer(DomainPanel, DomainsSizer), + + wxNotebook:addPage(Notebook, DomainPanel, "Domains"), + wxNotebook:setSelection(Notebook, 1), + + Domains = virtuerl_mgt:domains_list(), + ColumnNames = ["ID", "Name", "IPs"], + DomainsTuples = [{Id, Name, lists:join($,, [virtuerl_net:format_ip(Ip) || {Ip, _Prefixlen} <- Cidrs])} || #{id := Id, name := Name, cidrs := Cidrs} <- Domains], + DomainsIds = [Id || {Id, _Name, _} <- DomainsTuples], + DomainListBox = wxListCtrl:new(DomainSplitter, [{style, ?wxLC_REPORT}]), + lists:foreach(fun ({Idx, Name}) -> wxListCtrl:insertColumn(DomainListBox, Idx, Name) end, lists:enumerate(0, ColumnNames)), + lists:foreach(fun (Idx) -> + Item = wxListItem:new(), + wxListItem:setId(Item, Idx), + wxListCtrl:insertItem(DomainListBox, Item) + end, lists:seq(1, length(DomainsTuples))), + lists:foreach(fun (ColIdx) -> + lists:foreach(fun ({RowIdx, Dom}) -> + wxListCtrl:setItem(DomainListBox, RowIdx, ColIdx - 1, element(ColIdx, Dom)) + end, lists:enumerate(0, DomainsTuples)) + end, lists:seq(1, length(ColumnNames))), + wxListCtrl:connect(DomainListBox, command_list_item_selected), % command_listbox_doubleclicked + + DomainInfo = wxPanel:new(DomainSplitter), + DomainInfoSizer = wxBoxSizer:new(?wxVERTICAL), +%% DomainInfoGrid = wxFlexGridSizer:new(3, 2, 0, 0), + DomainInfoGrid = wxGridBagSizer:new(), + wxSizer:add(DomainInfoSizer, DomainInfoGrid, [{flag, ?wxEXPAND}, {proportion, 1}]), + wxPanel:setSizer(DomainInfo, DomainInfoSizer), + + DomainButtonsSizer = wxBoxSizer:new(?wxHORIZONTAL), + DomainDupBtn = wxButton:new(DomainInfo, 4044, [{label, "Duplicate"}]), + + wxButton:connect(DomainDupBtn, command_button_clicked), + DomainDelBtn = wxButton:new(DomainInfo, ?wxID_ANY, [{label, "Delete"}]), + wxSizer:add(DomainButtonsSizer, DomainDupBtn), + wxSizer:add(DomainButtonsSizer, DomainDelBtn), + wxSizer:add(DomainInfoSizer, DomainButtonsSizer, [{flag, ?wxALIGN_RIGHT}]), + + wxSplitterWindow:splitVertically(DomainSplitter, DomainListBox, DomainInfo, [{sashPosition, 25 * Mx}]), + % END Domains + + ok = wxFrame:setStatusText(Frame, "Hello World!",[]), + wxWindow:fit(Frame), + wxWindow:show(Frame), + wxWindow:raise(Frame), + wxWindow:setFocus(Frame), + wxWindow:layout(Frame), + {Frame, #state{win=Frame, info_panel = Info, info=InfoGrid, domain_panel = DomainInfo, domain_info=DomainInfoGrid, domain_list_box=DomainListBox, toolbar=Toolbar, domains = DomainsIds}}. + + +%% Handled as in normal gen_server callbacks +handle_info({domain_out, _Id, Text}, #state{domain_panel = DomainPanel} = State) -> + SerialOut = wx:typeCast(wxPanel:findWindow(DomainPanel, 69), wxStyledTextCtrl), + io:put_chars(Text), + wxStyledTextCtrl:appendText(SerialOut, Text), + wxStyledTextCtrl:scrollToLine(SerialOut, wxStyledTextCtrl:getLineCount(SerialOut)), + {noreply,State}; +handle_info(Msg, #state{domain_panel = DomainPanel} = State) -> + io:format("Got Info ~p~n",[Msg]), + {noreply, State}. + +handle_call(Msg, _From, State) -> + io:format("Got Call ~p~n",[Msg]), + {reply,ok,State}. + +%% Async Events are handled in handle_event as in handle_info +handle_event(#wx{id = 42, event = #wxCommand{type = command_listbox_selected, + cmdString = Choice}}, + State = #state{info_panel = Panel, info=Info, domains = DomainIds}) -> + {ok, Nets} = virtuerl_ipam:ipam_list_nets(), + Net = maps:get(list_to_binary(Choice), Nets), + #{cidr4 := #{address := Address, prefixlen := Prefixlen}} = Net, + wxFlexGridSizer:clear(Info, [{delete_windows, true}]), + wxSizer:add(Info, wxStaticText:new(Panel, -1, "CIDR")), + wxSizer:add(Info, wxStaticText:new(Panel, -1, iolist_to_binary([Address, "/", integer_to_binary(Prefixlen)]))), + io:format("dblclick ~p (~p)~n", [Choice, Net]), + {noreply, State}; +handle_event(#wx{event = #wxList{type = command_list_item_selected, + itemIndex = ItemIndex}}, + State = #state{domain_panel = DomainPanel, domain_info = DomainInfo, toolbar=Toolbar, domains = DomainIds}) -> + Domains = maps:from_list([{Id, Domain} || Domain = #{id := Id} <- virtuerl_mgt:domains_list()]), + wxGridBagSizer:clear(DomainInfo, [{delete_windows, true}]), + + DomainId = lists:nth(ItemIndex + 1, DomainIds), + Domain = maps:get(DomainId, Domains), + #{state := DomainState, network_id := NetworkId} = Domain, + case DomainState of + running -> + wxToolBar:enableTool(Toolbar, 100, false), + wxToolBar:enableTool(Toolbar, 101, true); + stopped -> + wxToolBar:enableTool(Toolbar, 100, true), + wxToolBar:enableTool(Toolbar, 101, false) + end, + wxGridBagSizer:add(DomainInfo, wxStaticText:new(DomainPanel, -1, "ID"), {0, 0}), + wxGridBagSizer:add(DomainInfo, wxStaticText:new(DomainPanel, -1, DomainId), {0, 1}), + wxGridBagSizer:add(DomainInfo, wxStaticText:new(DomainPanel, -1, "Network ID"), {1, 0}), + wxGridBagSizer:add(DomainInfo, wxStaticText:new(DomainPanel, -1, NetworkId), {1, 1}), + + DomainWithoutUserData = maps:remove(user_data, Domain), + wxGridBagSizer:add(DomainInfo, wxStaticText:new(DomainPanel, -1, io_lib:format("~p", [DomainWithoutUserData])), {2, 0}, [{span, {1, 2}}]), + +%% SerialOut = wxStyledTextCtrl:new(DomainPanel, [{id, 69}]), +%% wxStyledTextCtrl:setLexer(SerialOut, ?wxSTC_LEX_ERRORLIST), +%% wxStyledTextCtrl:styleSetVisible(SerialOut, 23, false), +%% wxStyledTextCtrl:styleSetVisible(SerialOut, 24, false), +%% wxStyledTextCtrl:setProperty(SerialOut, "lexer.errorlist.value.separate", "0"), +%% wxStyledTextCtrl:setProperty(SerialOut, "lexer.errorlist.escape.sequences", "1"), +%% wxGridBagSizer:add(DomainInfo, SerialOut, {3, 0}, [{span, {1, 2}}, {flag, ?wxEXPAND}]), +%% WebView = wxWebView:new(DomainPanel, 999, [{url, "http://0.0.0.0:9000/noVNC-1.4.0/vnc.html?port=5700&?path=&resize=scale&autoconnect=true"}]), +%% wxGridBagSizer:add(DomainInfo, WebView, {3, 0}, [{span, {1, 2}}, {flag, ?wxEXPAND}]), + VncWindow = virtuerl_vnc:start(DomainPanel), + wxGridBagSizer:add(DomainInfo, VncWindow, {3, 0}, [{span, {1, 2}}, {flag, ?wxEXPAND}]), + + wxGridBagSizer:addGrowableRow(DomainInfo, 3), + + UserData = maps:get(user_data, Domain, ""), + UserDataCtrl = wxStyledTextCtrl:new(DomainPanel), + wxStyledTextCtrl:setScrollWidth(UserDataCtrl, wxStyledTextCtrl:textWidth(UserDataCtrl, wxStyledTextCtrl:getStyleAt(UserDataCtrl, 0), UserData)), + wxStyledTextCtrl:setText(UserDataCtrl, list_to_binary(UserData)), + wxStyledTextCtrl:setReadOnly(UserDataCtrl, true), + LastRowIndex = 4, +%% Msg = wxNotificationMessage:new(lists:flatten(io_lib:format("LastRowIndex: ~p~n", [LastRowIndex]))), +%% wxNotificationMessage:show(Msg), + wxGridBagSizer:add(DomainInfo, UserDataCtrl, {LastRowIndex, 0}, [{span, {1, 2}}, {flag, ?wxEXPAND}]), + wxGridBagSizer:addGrowableRow(DomainInfo, LastRowIndex), + wxGridBagSizer:addGrowableCol(DomainInfo, 1), + wxPanel:layout(DomainPanel), + io:format("dblclick ~p (~p)~n", [DomainId, Domain]), + {noreply, State}; +handle_event(#wx{id = 4044, event = #wxCommand{type = command_button_clicked}}, #state{domain_list_box = DomainListBox, domains = DomainIds} = State) -> + SelectedItem = wxListCtrl:getNextItem(DomainListBox, -1, [{state, ?wxLIST_STATE_SELECTED}]), + DomainId = lists:nth(SelectedItem + 1, DomainIds), + {ok, Domain} = virtuerl_mgt:domain_get(#{id => DomainId}), + create_domain_dialog(Domain), + {noreply, State}; +handle_event(#wx{id = 100, obj = Toolbar, event = #wxCommand{type = command_menu_selected}} = Event, #state{domain_list_box = DomainListBox, domains = DomainIds} = State) -> + io:format("~p~n", [Event]), + SelectedItem = wxListCtrl:getNextItem(DomainListBox, -1, [{state, ?wxLIST_STATE_SELECTED}]), + DomainId = lists:nth(SelectedItem + 1, DomainIds), + Choice = DomainId, + io:format("~p~n", [Choice]), + wxToolBar:enableTool(Toolbar, 100, false), + wxToolBar:enableTool(Toolbar, 101, true), + ok = virtuerl_mgt:domain_start(Choice), + {noreply, State}; +handle_event(#wx{id = 101, obj = Toolbar, event = #wxCommand{type = command_menu_selected}} = Event, #state{domain_list_box = DomainListBox, domains = DomainIds} = State) -> + io:format("~p~n", [Event]), + SelectedItem = wxListCtrl:getNextItem(DomainListBox, -1, [{state, ?wxLIST_STATE_SELECTED}]), + DomainId = lists:nth(SelectedItem + 1, DomainIds), + Choice = DomainId, + io:format("~p~n", [Choice]), + wxToolBar:enableTool(Toolbar, 100, true), + wxToolBar:enableTool(Toolbar, 101, false), + ok = virtuerl_mgt:domain_stop(Choice), + {noreply, State}; +handle_event(#wx{event=#wxClose{}}, State = #state{win=Frame}) -> + io:format("~p Closing window ~n",[self()]), + ok = wxFrame:setStatusText(Frame, "Closing...",[]), + wxWindow:destroy(Frame), + {stop, normal, State}; +handle_event(#wx{id = ?wxID_EXIT, event = #wxCommand{type = command_menu_selected}}, State = #state{win=Frame}) -> + io:format("~p Quitting window ~n",[self()]), + ok = wxFrame:setStatusText(Frame, "Closing...",[]), + wxWindow:destroy(Frame), + {stop, normal, State}. + +code_change(_, _, State) -> + {stop, not_yet_implemented, State}. + +terminate(_Reason, _State) -> + ok. + +create_domain_dialog(#{network_id := NetworkId, user_data := UserData} = Domain) -> + {ok, Nets} = virtuerl_ipam:ipam_list_nets(), + Choices = maps:keys(Nets), + Dialog = wxDialog:new(wx:null(), ?wxID_ANY, "Create Domain", [{size, {1000, 500}}]), + DialogSizer = wxBoxSizer:new(?wxVERTICAL), + DialogGridSizer = wxFlexGridSizer:new(1, 2, 0, 0), + wxSizer:add(DialogGridSizer, wxStaticText:new(Dialog, ?wxID_ANY, "Network")), + NetworkChoice = wxChoice:new(Dialog, ?wxID_ANY, [{choices, Choices}]), + wxChoice:setStringSelection(NetworkChoice, NetworkId), + wxSizer:add(DialogGridSizer, NetworkChoice), + wxSizer:add(DialogSizer, DialogGridSizer), + UserDataCtrl = wxStyledTextCtrl:new(Dialog), + wxStyledTextCtrl:setLexer(UserDataCtrl, ?wxSTC_LEX_YAML), + wxStyledTextCtrl:setText(UserDataCtrl, UserData), + wxSizer:add(DialogSizer, UserDataCtrl, [{flag, ?wxEXPAND}, {proportion, 1}]), + ButtonSizer = wxDialog:createStdDialogButtonSizer(Dialog, ?wxOK bor ?wxCANCEL), + wxSizer:add(DialogSizer, ButtonSizer), + wxPanel:setSizer(Dialog, DialogSizer), + + case wxDialog:showModal(Dialog) of + ?wxID_OK -> + io:format("true ~p~n", [wxChoice:getStringSelection(NetworkChoice)]), + virtuerl_mgt:domain_create(#{network_id => list_to_binary(wxChoice:getStringSelection(NetworkChoice)), user_data => wxStyledTextCtrl:getText(UserDataCtrl)}); + _ -> ok + end, + wxDialog:destroy(Dialog); +create_domain_dialog(Domain) -> + create_domain_dialog(Domain#{user_data => ""}). diff --git a/virtuerl/src/virtuerl_vnc.erl b/virtuerl/src/virtuerl_vnc.erl new file mode 100644 index 0000000..02caf79 --- /dev/null +++ b/virtuerl/src/virtuerl_vnc.erl @@ -0,0 +1,239 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2009-2021. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%% +%% %CopyrightEnd% +%% +%%%------------------------------------------------------------------- +%%% File : hello.erl +%%% Author : Matthew Harrison +%%% Description : _really_ minimal example of a wxerlang app +%%% implemented with wx_object behaviour +%%% +%%% Created : 18 Sep 2008 by Matthew Harrison +%%% Dan rewrote it to show wx_object behaviour +%%%------------------------------------------------------------------- +-module(virtuerl_vnc). +-include_lib("wx/include/wx.hrl"). +-include_lib("wx/include/gl.hrl"). + +-export([ + init/1, handle_info/2, handle_event/2, + code_change/3, terminate/2]). +-export([start/1]). + +-behaviour(wx_object). + +-define(vncINVALID, 0). +-define(vncNONE, 1). +-define(vncVNC_AUTH, 2). +-define(vncOK, 0). +-define(vncFAILED, 1). +-define(vncMSG_TYPE_FramebufferUpdate, 0). +-define(vncMSG_TYPE_FramebufferUpdateRequest, 3). +-define(vncMSG_TYPE_KeyEvent, 4). +-define(vncMSG_TYPE_PointerEvent, 5). +-define(vncENCODING_RAW, 0). + +-record(state, {win, socket, panel, tex_id, vnc_conf}). +-record(vnc_conf, {width, height}). + +start(Parent) -> + wx_object:start_link(?MODULE, [Parent], []). + +%% Init is called in the new process. +init([Parent]) -> + {ok, Socket} = gen_tcp:connect("localhost", 5901, [{active, true}, binary]), + receive + {tcp, Socket, <<"RFB ", Major:3/binary, ".", Minor:3/binary, "\n">>} -> + io:format("Major: ~s / Minor: ~s~n", [Major, Minor]), + ok + after 2000 -> + timeout + end, + + ok = gen_tcp:send(Socket, <<"RFB 003.008\n">>), + receive + {tcp, Socket, <>} -> + io:format("AuthType: ~p~n", [AuthType]), + case AuthType of + ?vncINVALID -> {error, auth_invalid}; + ?vncNONE -> ok; + ?vncVNC_AUTH -> {error, auth_not_supported}; + _ -> io:format("Unknown auth: ~p~n", [AuthType]) + end + after 2000 -> + {error, timeout} + end, + + ok = gen_tcp:send(Socket, <>), + receive + {tcp, Socket, <>} -> + io:format("Security result: ~B~n", [SecurityResult]), + case SecurityResult of + ?vncOK -> ok; + ?vncFAILED -> error + end + after 2000 -> + {error, timeout} + end, + + ok = gen_tcp:send(Socket, <<1>>), + VncConf = receive + {tcp, Socket, <>} -> + io:format("~s: ~Bx~B~n~B/~B (BE: ~B, TC: ~B)~nR:(X>>~B)&~B G:(X>>~B)&~B B:(X>>~B)&~B~n", [Name, Width, Height, Depth, BitsPerPixel, BigEndianFlag, TrueColorFlag, RedShift, RedMax, GreenShift, GreenMax, BlueShift, BlueMax]), + #vnc_conf{width = Width, height = Height} + after 2000 -> + {error, timeout} + end, + #vnc_conf{width = VncWidth, height = VncHeight} = VncConf, +%% #vnc_conf{width = Width, height = Height} = #vnc_conf{width = 800, height = 600}, + inet:setopts(Socket, [{active, once}]), + ok = gen_tcp:send(Socket, <>), + + {Mx, My, _, _} = wxWindow:getTextExtent(Parent, "M"), +%% wxFrame:setClientSize(Frame, {60*Mx, 20*My}), + Panel = wxGLCanvas:new(Parent, [{attribList, [?WX_GL_RGBA,?WX_GL_DOUBLEBUFFER,0]}]), + Context = wxGLContext:new(Panel), +%% wxWindow:setMinSize(Panel, {VncWidth, VncHeight}), + + wxGLCanvas:connect(Panel, key_down), + wxGLCanvas:connect(Panel, key_up), + + wxGLCanvas:setCurrent(Panel, Context), + +gl:enable(?GL_TEXTURE_2D), + [TexId] = gl:genTextures(1), + io:format("TEXTURE ID: ~p~n", [TexId]), + gl:bindTexture(?GL_TEXTURE_2D, TexId), + gl:texImage2D(?GL_TEXTURE_2D, 0, ?GL_RGB, VncWidth, VncHeight, 0, ?GL_RGB, ?GL_UNSIGNED_BYTE, 0), + gl:texParameteri(?GL_TEXTURE_2D,?GL_TEXTURE_MIN_FILTER,?GL_LINEAR), + Err = gl:getError(), + io:format("ERROR: ~p~n", [glu:errorString(Err)]), + wxGLCanvas:connect(Panel, erase_background, [{callback, fun(_,_) -> ok end}]), + wxGLCanvas:connect(Panel, paint), + {Panel, #state{socket=Socket, panel = Panel, tex_id = TexId, vnc_conf = VncConf}}. + +read_rects(0, _, Socket) -> + []; +read_rects(NumRects, Data, Socket) -> + case Data of + <> -> + case Encoding of + ?vncENCODING_RAW -> + NumBytes = Width * Height * 4, + case NumBytes > byte_size(Rest) of + true -> + BytesToFetch = NumBytes - byte_size(Rest) + (NumRects - 1) * 12, + {ok, MoreData} = gen_tcp:recv(Socket, BytesToFetch), + read_rects(NumRects, <>, Socket); + false -> + <> = Rest, + Rect = {X, Y, Width, Height, PixelBytes}, + Rects = read_rects(NumRects - 1, ActualRest, Socket), + [Rect | Rects] + end; + _ -> io:format("Unsupported encoding ~p~n", [Encoding]) + end; + _ -> + {ok, MoreData} = gen_tcp:recv(Socket, 12 - byte_size(Data) + (NumRects - 1) * 12), + read_rects(NumRects, <>, Socket) + end. + +%% Handled as in normal gen_server callbacks +handle_info({tcp, Socket, <>}, #state{panel = Panel, tex_id = TexId, vnc_conf = #vnc_conf{width = Width, height = Height}} = State) -> + {TimeRects, Rects} = timer:tc(fun() -> read_rects(NumRects, Rest, Socket) end), + {TimeRes, _} = timer:tc(fun() -> + wx:batch(fun() -> + gl:bindTexture(?GL_TEXTURE_2D, TexId), + [gl:texSubImage2D(?GL_TEXTURE_2D, 0, X, Y, Width, Height, ?GL_BGRA, ?GL_UNSIGNED_BYTE, Data) || {X, Y, Width, Height, Data} <- Rects] + end), + gl:flush() + end), + wxPanel:refresh(Panel, [{eraseBackground, false}]), +%% io:format("Writing rects took ~p/~p~n", [TimeRects, TimeRes]), + + inet:setopts(Socket, [{active, once}]), + ok = gen_tcp:send(Socket, <>), + {noreply, State}; + +handle_info(Msg, State) -> + io:format("Got Info ~p~n",[Msg]), + {noreply,State}. + +handle_event(#wx{event=#wxPaint{type = paint}, obj = _Obj}, State = #state{win=Frame, panel=Panel, tex_id = TexId, vnc_conf = #vnc_conf{width = Width, height = Height}}) -> + {W, H} = wxGLCanvas:getSize(_Obj), + Wscale = W / Width, + Hscale = H / Height, + {Scale, Xt, Yt} = case Wscale > Hscale of + true -> + {Hscale, max((W - Width*Hscale) / 2, 0), 0}; + false -> + {Wscale, 0, max((H - Height*Wscale) / 2, 0)} + end, + + {ScaleX, ScaleY} = {Width * Scale / W, Height * Scale / H}, + gl:viewport(0,0,W,H), + + gl:matrixMode(?GL_MODELVIEW), + gl:loadIdentity(), + gl:scalef(ScaleX, ScaleY, 0.0), + gl:clearColor(0.2, 0.2, 0.2, 1.0), + gl:clear(?GL_COLOR_BUFFER_BIT bor ?GL_DEPTH_BUFFER_BIT), + gl:bindTexture(?GL_TEXTURE_2D, TexId), + gl:enable(?GL_TEXTURE_2D), + gl:'begin'(?GL_QUADS), + + gl:texCoord2f(0.0, 1.0), + gl:vertex2f(-1.0, -1.0), + gl:texCoord2f(0.0, 0.0), + gl:vertex2f(-1.0, 1.0), + gl:texCoord2f(1.0, 0.0), + gl:vertex2f( 1.0, 1.0), + gl:texCoord2f(1.0, 1.0), + gl:vertex2f( 1.0, -1.0), + + gl:'end'(), + gl:flush(), + wxGLCanvas:swapBuffers(_Obj), + {noreply, State}; +handle_event(#wx{event=#wxKey{type = Type, keyCode = KeyCode, rawCode = Key}} = Event, State = #state{win=Frame, panel=Panel, socket = Socket}) -> + io:format("Event ~p~n", [Event]), + DownFlag = case Type of + key_down -> 1; + key_up -> 0 + end, + ok = gen_tcp:send(Socket, <>), + {noreply, State}; +handle_event(#wx{id = ?wxID_EXIT, event = #wxCommand{type = command_menu_selected}}, State = #state{win=Frame}) -> + io:format("~p Quitting window ~n",[self()]), + ok = wxFrame:setStatusText(Frame, "Closing...",[]), + wxWindow:destroy(Frame), + {stop, normal, State}; +handle_event(Event, State) -> + io:format("Got Event ~p~n",[Event]), + {noreply, State}. + +code_change(_, _, State) -> + {stop, not_yet_implemented, State}. + +terminate(_Reason, _State) -> + ok.