From 8fc0b7e5d2d8163fd47dbc26e6331b3213c92db3 Mon Sep 17 00:00:00 2001 From: Ilya Verbitskiy Date: Sun, 11 Feb 2024 00:11:08 +0100 Subject: [PATCH] virtuerl: more steps towards a MVP --- virtuerl/src/virtuerl_ipam.erl | 10 ++ virtuerl/src/virtuerl_mgt.erl | 17 +-- virtuerl/src/virtuerl_net.erl | 6 +- virtuerl/src/virtuerl_qemu.erl | 18 +-- virtuerl/src/virtuerl_ui.erl | 189 +++++++++++++++++++++------- virtuerl/src/virtuerl_vnc.erl | 123 ++++-------------- virtuerl/src/virtuerl_vnc_proxy.erl | 143 +++++++++++++++++++++ 7 files changed, 348 insertions(+), 158 deletions(-) create mode 100644 virtuerl/src/virtuerl_vnc_proxy.erl diff --git a/virtuerl/src/virtuerl_ipam.erl b/virtuerl/src/virtuerl_ipam.erl index 3eb23a4..b332565 100644 --- a/virtuerl/src/virtuerl_ipam.erl +++ b/virtuerl/src/virtuerl_ipam.erl @@ -8,6 +8,7 @@ -behavior(gen_server). -export([req/1, init/1, handle_call/3, subnet/1, get_range/1, get_next/6, terminate/2, ipam_next_ip/1, start_server/1, stop_server/1, ipam_put_net/1, start_link/0, handle_cast/2, ipam_delete_net/1, ipam_create_net/1, ipam_list_nets/0, ipam_get_net/1, ipam_put_ip/3, assign_next/3]). +-export([unassign/1]). -include_lib("khepri/include/khepri.hrl"). -include_lib("khepri/src/khepri_error.hrl"). @@ -51,6 +52,8 @@ ipam_put_net(NetworkDef) -> ipam_list_nets() -> gen_server:call(ipam, net_list). +ipam_delete_net(ID) when is_list(ID) -> + ipam_delete_net(list_to_binary(ID)); ipam_delete_net(ID) -> case gen_server:call(ipam, {net_delete, ID}) of {ok, Res} -> @@ -82,6 +85,9 @@ assign_next(NetworkID, Tag, VMID) -> Other end. +unassign(DomainId) -> + gen_server:call(ipam, {unassign, DomainId}). + ipam_next_ip(NetworkName) -> case gen_server:call(ipam, {ip_next, NetworkName}) of {ok, Res} -> @@ -202,6 +208,10 @@ handle_call({ip_delete, NetworkId, IpAddr}, _From, StoreId) -> R = khepri:delete(StoreId, [network, NetworkId, ?KHEPRI_WILDCARD_STAR, IpAddr]), {reply, R, StoreId}; +handle_call({unassign, DomainId}, _From, StoreId) -> + ok = khepri:delete_many(StoreId, [network, ?KHEPRI_WILDCARD_STAR, ?KHEPRI_WILDCARD_STAR, #if_data_matches{pattern = DomainId}]), + {reply, ok, 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 b52c921..31b4d4f 100644 --- a/virtuerl/src/virtuerl_mgt.erl +++ b/virtuerl/src/virtuerl_mgt.erl @@ -61,8 +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(), "openSUSE-Leap-15.5.x86_64-NoCloud.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 -> @@ -111,7 +111,7 @@ generate_unique_tap_name(TapNames) -> handle_call({domain_create, Conf}, _From, State) -> {Table} = State, DomainID = virtuerl_util:uuid4(), - Domain = maps:merge(#{id => DomainID, name => DomainID}, Conf), % TODO: save ipv4 addr as well + Domain = maps:merge(#{id => DomainID, name => DomainID, vcpu => 1, memory => 512}, Conf), % TODO: save ipv4 addr as well dets:insert_new(Table, {DomainID, Domain}), dets:sync(Table), @@ -182,24 +182,27 @@ handle_call({domain_update, #{id := DomainID, state := RunState}}, _From, {Table 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}; + {reply, [maps:merge(#{state => running, name => Id, vcpu => 1, memory => 512}, Domain) || {Id, Domain} <- Domains], State}; handle_call({domain_get, #{id := DomainID}}, _From, State) -> {Table} = State, Reply = case dets:lookup(Table, DomainID) of [{_, #{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)}; + {ok, maps:merge(#{state => running, name => DomainID, vcpu => 1, memory => 512}, DomRet)}; [] -> notfound end, {reply, Reply, State}; handle_call({domain_delete, #{id := DomainID}}, _From, State) -> {Table} = State, + ok = virtuerl_ipam:unassign(DomainID), Res = dets:delete(Table, DomainID), dets:sync(Table), io:format("terminating ~p~n", [DomainID]), - ok = supervisor:terminate_child(virtuerl_sup, DomainID), + supervisor:terminate_child(virtuerl_sup, DomainID), io:format("done terminating ~p~n", [DomainID]), - ok = supervisor:delete_child(virtuerl_sup, DomainID), + supervisor:delete_child(virtuerl_sup, DomainID), + DomainHomePath = filename:join([virtuerl_mgt:home_path(), "domains", DomainID]), + file:del_dir_r(DomainHomePath), 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 01d9a5d..6031ad4 100644 --- a/virtuerl/src/virtuerl_net.erl +++ b/virtuerl/src/virtuerl_net.erl @@ -142,7 +142,7 @@ update_nftables(Domains) -> " chain postrouting {\n", " type nat hook postrouting priority -5; policy accept;\n", - " oifname != \"verlbr*\" iifname \"verlbr*\" masquerade\n", + " oifname != \"verlbr*\" iifname \"verlbr*\" masquerade\n", % TODO: we need to base it on IPs " }\n", "}\n" ], @@ -258,7 +258,7 @@ generate_unique_bridge_name(Ifnames) -> generate_unique_bridge_name(Ifnames) end. -to_vtap_map(MacAddr) -> +to_vtap_mac(MacAddr) -> <> = MacAddr, <>. @@ -274,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), {to_vtap_map(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_mac(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_qemu.erl b/virtuerl/src/virtuerl_qemu.erl index 9ce0f7f..3981c5a 100644 --- a/virtuerl/src/virtuerl_qemu.erl +++ b/virtuerl/src/virtuerl_qemu.erl @@ -39,22 +39,24 @@ 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} = DomainRaw}] = dets:lookup(Table, ID), - Domain = maps:merge(#{user_data => ""}, DomainRaw), + Domain = maps:merge(#{user_data => "", vcpu => 1, memory => 512}, 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"]), - 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]); + 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 20G", [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), " -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 + file:delete(filename:join(DomainHomePath, "vnc.sock")), + #{vcpu := Vcpu, memory := Memory} = Domain, + 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 unix:vnc.sock -display none -serial none -smp ",integer_to_binary(Vcpu)," -m ",integer_to_binary(Memory)," -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}, @@ -179,7 +181,7 @@ ensure_cloud_config(#{id := DomainID} = Domain) -> false -> create_cloud_config(Domain) end. -create_cloud_config(#{id := DomainID, mac_addr := MacAddr, cidrs := Cidrs, user_data := UserData}) -> +create_cloud_config(#{id := DomainID, name := DomainName, mac_addr := MacAddr, cidrs := Cidrs, user_data := UserData}) -> NetConf = [ "version: 2\n", "ethernets:\n", @@ -189,6 +191,8 @@ create_cloud_config(#{id := DomainID, mac_addr := MacAddr, cidrs := Cidrs, user_ " set-name: ens2\n", " dhcp4: false\n", " dhcp6: false\n", + " nameservers:\n", + " addresses: [8.8.8.8, 8.8.4.4]\n", " addresses:\n", [[ " - ", virtuerl_net:format_ip(IpAddr), "/", integer_to_binary(Prefixlen), "\n"] || {IpAddr, Prefixlen} <- Cidrs], " routes:\n", [[ @@ -198,7 +202,7 @@ create_cloud_config(#{id := DomainID, mac_addr := MacAddr, cidrs := Cidrs, user_ ], MetaData = [ "instance-id: ", DomainID, "\n", - "local-hostname: ", DomainID, "\n" + "local-hostname: ", DomainName, "\n" ], DomainBasePath = filename:join([virtuerl_mgt:home_path(), "domains", DomainID]), IsoBasePath = filename:join(DomainBasePath, "iso"), diff --git a/virtuerl/src/virtuerl_ui.erl b/virtuerl/src/virtuerl_ui.erl index ca2238f..3718022 100644 --- a/virtuerl/src/virtuerl_ui.erl +++ b/virtuerl/src/virtuerl_ui.erl @@ -35,7 +35,7 @@ -behaviour(wx_object). --record(state, {win, info_panel, info, domain_panel, domain_info, toolbar, domain_list_box, domains}). +-record(state, {win, info_panel, info, domain_panel, domain_info, toolbar, domain_list_box, domains, page, net_list_box}). start() -> wx_object:start_link(?MODULE, [], []). @@ -60,31 +60,7 @@ init([]) -> {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), + Toolbar = create_toolbar(Frame, 100), wxFrame:createStatusBar(Frame,[]), @@ -119,7 +95,7 @@ init([]) -> wxSizer:add(InfoSizer, InfoGrid, [{flag, ?wxEXPAND}, {proportion, 1}]), wxPanel:setSizer(Info, InfoSizer), - wxSplitterWindow:splitVertically(NetworkSplitter, ListBox, Info, [{sashPosition, 25 * Mx}]), + wxSplitterWindow:splitHorizontally(NetworkSplitter, ListBox, Info, [{sashPosition, 25 * Mx}]), % BEGIN Domains DomainPanel = wxPanel:new(Notebook, []), @@ -129,14 +105,16 @@ init([]) -> wxPanel:setSizer(DomainPanel, DomainsSizer), wxNotebook:addPage(Notebook, DomainPanel, "Domains"), + wxNotebook:connect(Notebook, command_notebook_page_changed), 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], + ColumnNames = ["ID", "Name", "IPs", "CPU", "RAM"], + ColumnAlignment = [?wxLIST_FORMAT_LEFT, ?wxLIST_FORMAT_LEFT, ?wxLIST_FORMAT_LEFT, ?wxLIST_FORMAT_RIGHT, ?wxLIST_FORMAT_RIGHT], + DomainsTuples = [{Id, Name, lists:join($,, [virtuerl_net:format_ip(Ip) || {Ip, _Prefixlen} <- Cidrs]), integer_to_binary(Vcpu), [integer_to_binary(Memory), $M]} || #{id := Id, name := Name, cidrs := Cidrs, vcpu := Vcpu, memory := Memory} <- 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, Name}) -> wxListCtrl:insertColumn(DomainListBox, Idx, Name, [{format, lists:nth(Idx + 1, ColumnAlignment)}]) end, lists:enumerate(0, ColumnNames)), lists:foreach(fun (Idx) -> Item = wxListItem:new(), wxListItem:setId(Item, Idx), @@ -165,7 +143,7 @@ init([]) -> wxSizer:add(DomainButtonsSizer, DomainDelBtn), wxSizer:add(DomainInfoSizer, DomainButtonsSizer, [{flag, ?wxALIGN_RIGHT}]), - wxSplitterWindow:splitVertically(DomainSplitter, DomainListBox, DomainInfo, [{sashPosition, 25 * Mx}]), + wxSplitterWindow:splitHorizontally(DomainSplitter, DomainListBox, DomainInfo, [{sashPosition, 25 * Mx}]), % END Domains ok = wxFrame:setStatusText(Frame, "Hello World!",[]), @@ -174,8 +152,65 @@ init([]) -> 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}}. + {Frame, #state{toolbar = Toolbar, page = 1, win=Frame, net_list_box = ListBox, info_panel = Info, info=InfoGrid, domain_panel = DomainInfo, domain_info=DomainInfoGrid, domain_list_box=DomainListBox, domains = DomainsIds}}. + +create_toolbar(Frame, BaseNum) -> + 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))), + DelIconDC = wxMemoryDC:new(), + DelIcon = wxBitmap:new(30, 30, [{depth, 32}]), + wxBufferedDC:selectObject(DelIconDC, DelIcon), + wxMemoryDC:setBrush(DelIconDC, ?wxBLUE_BRUSH), + wxMemoryDC:setPen(DelIconDC, ?wxBLACK_PEN), + wxMemoryDC:drawRectangle(DelIconDC, {3,10},{24,20}), + wxMemoryDC:drawRectangle(DelIconDC, {0,5},{30,5}), + wxMemoryDC:drawRectangle(DelIconDC, {10,0},{10,5}), + wxMemoryDC:setBrush(DelIconDC, ?wxWHITE_BRUSH), + wxMemoryDC:setPen(DelIconDC, ?wxWHITE_PEN), + wxMemoryDC:drawLine(DelIconDC, {9,14}, {9,26}), + wxMemoryDC:drawLine(DelIconDC, {15,14}, {15,26}), + wxMemoryDC:drawLine(DelIconDC, {21,14}, {21,26}), + wxMemoryDC:destroy(DelIconDC), + DelIconDisabled = wxBitmap:new(wxImage:convertToGreyscale(wxBitmap:convertToImage(DelIcon))), + + AddIconDC = wxMemoryDC:new(), + AddIcon = wxBitmap:new(30, 30, [{depth, 32}]), + wxBufferedDC:selectObject(AddIconDC, AddIcon), + wxMemoryDC:setBrush(AddIconDC, ?wxGREEN_BRUSH), + wxMemoryDC:setPen(AddIconDC, ?wxGREEN_PEN), + wxMemoryDC:drawRectangle(AddIconDC, {0,12},{30,6}), + wxMemoryDC:drawRectangle(AddIconDC, {12,0},{6,30}), + wxMemoryDC:destroy(AddIconDC), + AddIconDisabled = wxBitmap:new(wxImage:convertToGreyscale(wxBitmap:convertToImage(AddIcon))), + + Toolbar = wxFrame:createToolBar(Frame), + wxToolBar:addTool(Toolbar, BaseNum + 0, "test123", PlayIcon, PlayIconDisabled), + wxToolBar:enableTool(Toolbar, BaseNum + 0, false), + wxToolBar:addTool(Toolbar, BaseNum + 1, "test123", StopIcon, StopIconDisabled), + wxToolBar:enableTool(Toolbar, BaseNum + 1, false), + wxToolBar:addTool(Toolbar, BaseNum + 2, "test123", DelIcon, DelIconDisabled), + wxToolBar:enableTool(Toolbar, BaseNum + 2, false), + wxToolBar:addTool(Toolbar, BaseNum + 3, "test123", AddIcon, AddIconDisabled), + wxToolBar:enableTool(Toolbar, BaseNum + 3, true), + wxToolBar:realize(Toolbar), + wxToolBar:connect(Toolbar, command_menu_selected), + Toolbar. %% Handled as in normal gen_server callbacks handle_info({domain_out, _Id, Text}, #state{domain_panel = DomainPanel} = State) -> @@ -193,6 +228,9 @@ handle_call(Msg, _From, State) -> {reply,ok,State}. %% Async Events are handled in handle_event as in handle_info +handle_event(#wx{event = #wxBookCtrl{nSel = Index}}, State) -> + io:format("TAB: ~p~n", [Index]), + {noreply, State#state{page = Index}}; handle_event(#wx{id = 42, event = #wxCommand{type = command_listbox_selected, cmdString = Choice}}, State = #state{info_panel = Panel, info=Info, domains = DomainIds}) -> @@ -221,6 +259,7 @@ handle_event(#wx{event = #wxList{type = command_list_item_selected, wxToolBar:enableTool(Toolbar, 100, true), wxToolBar:enableTool(Toolbar, 101, false) end, + wxToolBar:enableTool(Toolbar, 102, true), 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}), @@ -238,22 +277,22 @@ handle_event(#wx{event = #wxList{type = command_list_item_selected, %% 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}]), + VncWindow = virtuerl_vnc:start(DomainPanel, DomainId), + wxGridBagSizer:add(DomainInfo, VncWindow, {0, 2}, [{span, {3, 1}}, {flag, ?wxEXPAND}]), - wxGridBagSizer:addGrowableRow(DomainInfo, 3), + wxGridBagSizer:addGrowableRow(DomainInfo, 2), 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, + LastRowIndex = 3, %% 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:add(DomainInfo, UserDataCtrl, {LastRowIndex, 0}, [{span, {1, 3}}, {flag, ?wxEXPAND}]), wxGridBagSizer:addGrowableRow(DomainInfo, LastRowIndex), - wxGridBagSizer:addGrowableCol(DomainInfo, 1), + wxGridBagSizer:addGrowableCol(DomainInfo, 2), wxPanel:layout(DomainPanel), io:format("dblclick ~p (~p)~n", [DomainId, Domain]), {noreply, State}; @@ -263,7 +302,7 @@ handle_event(#wx{id = 4044, event = #wxCommand{type = command_button_clicked}}, {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) -> +handle_event(#wx{id = 100, obj = Toolbar, event = #wxCommand{type = command_menu_selected}} = Event, #state{page = 1, 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), @@ -273,7 +312,7 @@ handle_event(#wx{id = 100, obj = Toolbar, event = #wxCommand{type = command_menu 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) -> +handle_event(#wx{id = 101, obj = Toolbar, event = #wxCommand{type = command_menu_selected}} = Event, #state{page = 1, 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), @@ -283,6 +322,29 @@ handle_event(#wx{id = 101, obj = Toolbar, event = #wxCommand{type = command_menu wxToolBar:enableTool(Toolbar, 101, false), ok = virtuerl_mgt:domain_stop(Choice), {noreply, State}; +handle_event(#wx{id = 102, obj = Toolbar, event = #wxCommand{type = command_menu_selected}} = Event, #state{page = 1, 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), + virtuerl_mgt:domain_delete(#{id => DomainId}), + {noreply, State}; +handle_event(#wx{id = 103, obj = Toolbar, event = #wxCommand{type = command_menu_selected}} = Event, #state{page = 1, domain_list_box = DomainListBox, domains = DomainIds} = State) -> + create_domain_dialog(#{network_id => "", name=> "", user_data => "", vcpu => 2, memory => 1024}), + {noreply, State}; +%% BEGIN: Network Toolbar +handle_event(#wx{id = 102, event = #wxCommand{type = command_menu_selected}}, #state{page = 0, net_list_box = ListBox} = State) -> + NetId = wxListBox:getStringSelection(ListBox), + virtuerl_ipam:ipam_delete_net(NetId), + {noreply, State}; +handle_event(#wx{id = 103, event = #wxCommand{type = command_menu_selected}}, #state{page = 0} = State) -> + create_network_dialog(), + {noreply, State}; +%% END: Network Toolbar handle_event(#wx{event=#wxClose{}}, State = #state{win=Frame}) -> io:format("~p Closing window ~n",[self()]), ok = wxFrame:setStatusText(Frame, "Closing...",[]), @@ -292,7 +354,10 @@ handle_event(#wx{id = ?wxID_EXIT, event = #wxCommand{type = command_menu_selecte io:format("~p Quitting window ~n",[self()]), ok = wxFrame:setStatusText(Frame, "Closing...",[]), wxWindow:destroy(Frame), - {stop, normal, State}. + {stop, normal, State}; +handle_event(Event, State) -> + io:format("Unknown Event: ~p~n", [Event]), + {noreply, State}. code_change(_, _, State) -> {stop, not_yet_implemented, State}. @@ -300,16 +365,46 @@ code_change(_, _, State) -> terminate(_Reason, _State) -> ok. -create_domain_dialog(#{network_id := NetworkId, user_data := UserData} = Domain) -> +create_network_dialog() -> + Dialog = wxDialog:new(wx:null(), ?wxID_ANY, "Create Network", [{size, {1000, 500}}]), + DialogSizer = wxBoxSizer:new(?wxVERTICAL), + DialogGridSizer = wxFlexGridSizer:new(1, 2, 0, 0), + wxSizer:add(DialogGridSizer, wxStaticText:new(Dialog, ?wxID_ANY, "CIDR")), + CidrCtrl = wxTextCtrl:new(Dialog, ?wxID_ANY), + wxSizer:add(DialogGridSizer, CidrCtrl, [{flag, ?wxEXPAND}, {proportion, 1}]), + wxSizer:add(DialogSizer, DialogGridSizer), + ButtonSizer = wxDialog:createStdDialogButtonSizer(Dialog, ?wxOK bor ?wxCANCEL), + wxSizer:add(DialogSizer, ButtonSizer), + wxPanel:setSizer(Dialog, DialogSizer), +%% wxWindow:layout(DialogSizer), + + case wxDialog:showModal(Dialog) of + ?wxID_OK -> + NetDef = virtuerl_net:parse_cidr(wxTextCtrl:getValue(CidrCtrl)), + {ok, NetId} = virtuerl_ipam:ipam_create_net([NetDef]); + _ -> ok + end, + wxDialog:destroy(Dialog). + +create_domain_dialog(#{network_id := NetworkId, name:= DomainName, user_data := UserData, vcpu := Vcpu, memory := Memory} = 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, "Name")), + NameCtrl = wxTextCtrl:new(Dialog, ?wxID_ANY, [{value, DomainName}]), + wxSizer:add(DialogGridSizer, NameCtrl, [{flag, ?wxEXPAND}, {proportion, 1}]), 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(DialogGridSizer, wxStaticText:new(Dialog, ?wxID_ANY, "CPU")), + VcpuCtrl = wxSpinCtrl:new(Dialog, [{min, 1}, {max, 256}, {initial, Vcpu}]), + wxSizer:add(DialogGridSizer, VcpuCtrl), + wxSizer:add(DialogGridSizer, wxStaticText:new(Dialog, ?wxID_ANY, "Memory")), + MemoryCtrl = wxSpinCtrl:new(Dialog, [{min, 128}, {max, 131072}, {initial, Memory}]), + wxSizer:add(DialogGridSizer, MemoryCtrl), wxSizer:add(DialogSizer, DialogGridSizer), UserDataCtrl = wxStyledTextCtrl:new(Dialog), wxStyledTextCtrl:setLexer(UserDataCtrl, ?wxSTC_LEX_YAML), @@ -322,7 +417,13 @@ create_domain_dialog(#{network_id := NetworkId, user_data := UserData} = Domain) 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)}); + virtuerl_mgt:domain_create(#{ + name => wxTextCtrl:getValue(NameCtrl), + network_id => list_to_binary(wxChoice:getStringSelection(NetworkChoice)), + user_data => wxStyledTextCtrl:getText(UserDataCtrl), + vcpu => wxSpinCtrl:getValue(VcpuCtrl), + memory => wxSpinCtrl:getValue(MemoryCtrl) + }); _ -> ok end, wxDialog:destroy(Dialog); diff --git a/virtuerl/src/virtuerl_vnc.erl b/virtuerl/src/virtuerl_vnc.erl index 02caf79..5dc343d 100644 --- a/virtuerl/src/virtuerl_vnc.erl +++ b/virtuerl/src/virtuerl_vnc.erl @@ -33,7 +33,7 @@ -export([ init/1, handle_info/2, handle_event/2, code_change/3, terminate/2]). --export([start/1]). +-export([start/2]). -behaviour(wx_object). @@ -48,67 +48,14 @@ -define(vncMSG_TYPE_PointerEvent, 5). -define(vncENCODING_RAW, 0). --record(state, {win, socket, panel, tex_id, vnc_conf}). +-record(state, {win, socket, panel, tex_id, vnc_conf, parent}). -record(vnc_conf, {width, height}). -start(Parent) -> - wx_object:start_link(?MODULE, [Parent], []). +start(Parent, DomainId) -> + wx_object:start_link(?MODULE, [Parent, DomainId], []). %% 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, <>), - +init([Parent, DomainId]) -> {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]}]), @@ -120,7 +67,17 @@ init([Parent]) -> wxGLCanvas:setCurrent(Panel, Context), -gl:enable(?GL_TEXTURE_2D), + VncProxy = virtuerl_vnc_proxy:start(DomainId), + {VncWidth, VncHeight} = receive + {conf, TVncWidth, TVncHeight} -> {TVncWidth, TVncHeight} + after 1000 -> + io:format("the foock?~n"), + {700, 400} + end, + + VncProxy ! {framebuffer_update_request, 0, 0, 0, VncWidth, VncHeight}, + + gl:enable(?GL_TEXTURE_2D), [TexId] = gl:genTextures(1), io:format("TEXTURE ID: ~p~n", [TexId]), gl:bindTexture(?GL_TEXTURE_2D, TexId), @@ -130,57 +87,29 @@ gl:enable(?GL_TEXTURE_2D), 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. + {Panel, #state{socket=VncProxy, parent = Parent, panel = Panel, tex_id = TexId, vnc_conf = {VncWidth, VncHeight}}}. %% 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() -> +handle_info({framebuffer_update, Rects}, #state{socket = Proxy, panel = Panel, tex_id = TexId, vnc_conf = {VncWidth, VncHeight}} = State) -> +%% io:format("got framebuffer_update~n"), 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), + gl:flush(), 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, <>), + + Proxy ! {framebuffer_update_request, 1, 0, 0, VncWidth, VncHeight}, {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), +handle_event(#wx{event=#wxPaint{type = paint}, obj = _Obj}, State = #state{parent = Parent, win=Frame, panel=Panel, tex_id = TexId, vnc_conf = {Width, Height}}) -> + {W, H} = wxGLCanvas:getSize(Panel), Wscale = W / Width, Hscale = H / Height, {Scale, Xt, Yt} = case Wscale > Hscale of @@ -213,7 +142,7 @@ handle_event(#wx{event=#wxPaint{type = paint}, obj = _Obj}, State = #state{win=F gl:'end'(), gl:flush(), - wxGLCanvas:swapBuffers(_Obj), + wxGLCanvas:swapBuffers(Panel), {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]), @@ -221,7 +150,7 @@ handle_event(#wx{event=#wxKey{type = Type, keyCode = KeyCode, rawCode = Key}} = key_down -> 1; key_up -> 0 end, - ok = gen_tcp:send(Socket, <>), + Socket ! {key_event, DownFlag, Key}, {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()]), diff --git a/virtuerl/src/virtuerl_vnc_proxy.erl b/virtuerl/src/virtuerl_vnc_proxy.erl new file mode 100644 index 0000000..cec75b5 --- /dev/null +++ b/virtuerl/src/virtuerl_vnc_proxy.erl @@ -0,0 +1,143 @@ +%%%------------------------------------------------------------------- +%%% @author ilya +%%% @copyright (C) 2023, +%%% @doc +%%% @end +%%%------------------------------------------------------------------- +-module(virtuerl_vnc_proxy). + +-export([start/1]). + +-define(SERVER, ?MODULE). +-define(APPLICATION, virtuerl). + +-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}). + +%%%=================================================================== +%%% Spawning and gen_server implementation +%%%=================================================================== + +start(DomainId) -> + VncSocketPath = filename:join([virtuerl_mgt:home_path(), "domains", DomainId, "vnc.sock"]), + Self = self(), + spawn(fun() -> init(VncSocketPath, Self) end). + +init(VncSocketPath, Sender) -> + {ok, Socket} = gen_tcp:connect({local, VncSocketPath}, 0, [{active, true}, binary, local]), + 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, <>} -> + % TODO: AuthTypes is a list + 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, + inet:setopts(Socket, [{active, once}]), + + Sender ! {conf, VncWidth, VncHeight}, + + monitor(process, Sender), + loop(Socket, Sender). + +loop(Socket, Sender) -> + receive + {tcp, Socket, <>} -> + {TimeRects, Rects} = timer:tc(fun() -> read_rects(NumRects, Rest, Socket) end), + Sender ! {framebuffer_update, Rects}, + inet:setopts(Socket, [{active, once}]), + loop(Socket, Sender); + {tcp, Socket,_Data} -> + io:format("Got data: ~p~n", [_Data]), + inet:setopts(Socket, [{active, once}]), + loop(Socket, Sender); + {framebuffer_update_request, Incremental, X, Y, Width, Height} -> + ok = gen_tcp:send(Socket, <>), + loop(Socket, Sender); + {key_event, DownFlag, Key} -> + ok = gen_tcp:send(Socket, <>), + loop(Socket, Sender); + {'DOWN', Ref, process, Pid, Reason} -> + io:format("Terminating because: ~p (~p)~n", [Reason, Pid]); + _Message -> + io:format("Got message: ~p~n", [_Message]), + loop(Socket, Sender) +end. + +read_rects(0, _Data, _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.