Skip to content

Commit 17d8202

Browse files
authored
Merge pull request #206 from open-watt/api
Add API handler
2 parents 72edca8 + 4e263d3 commit 17d8202

File tree

12 files changed

+416
-99
lines changed

12 files changed

+416
-99
lines changed

conf/startup.conf

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,3 +157,4 @@ add name=goodwe_can stream=can.1 protocol=ebyte
157157
/protocol/mdns/server add name=mdns
158158

159159
/protocol/websocket/server add name=websock http-server=webserver uri=/ws
160+
/apps/api add name=api http-server=webserver uri=/api

openwatt.vcxproj

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
</ProjectConfiguration>
5252
</ItemGroup>
5353
<ItemGroup>
54+
<DCompile Include="src\apps\api\package.d" />
5455
<DCompile Include="src\apps\energy\appliance.d" />
5556
<DCompile Include="src\apps\energy\circuit.d" />
5657
<DCompile Include="src\apps\energy\manager.d" />
@@ -484,6 +485,7 @@
484485
<CppStandard>fromCpp</CppStandard>
485486
<EnableDebugMixin>true</EnableDebugMixin>
486487
<AdditionalOptions>-preview=bitfields %(AdditionalOptions)</AdditionalOptions>
488+
<Warnings>None</Warnings>
487489
</DCompile>
488490
<Link>
489491
<SubSystem>Console</SubSystem>
@@ -513,6 +515,7 @@
513515
<Main>true</Main>
514516
<EnableDebugMixin>true</EnableDebugMixin>
515517
<AdditionalOptions>-preview=bitfields %(AdditionalOptions)</AdditionalOptions>
518+
<Warnings>None</Warnings>
516519
</DCompile>
517520
<Link>
518521
<SubSystem>Console</SubSystem>
@@ -545,6 +548,7 @@
545548
<BoundsCheck>Off</BoundsCheck>
546549
<EnableDebugMixin>true</EnableDebugMixin>
547550
<AdditionalOptions>-preview=bitfields %(AdditionalOptions)</AdditionalOptions>
551+
<Warnings>None</Warnings>
548552
</DCompile>
549553
<Link>
550554
<SubSystem>Console</SubSystem>
@@ -575,6 +579,7 @@
575579
<EnableDebugMixin>true</EnableDebugMixin>
576580
<AdditionalOptions>-preview=bitfields %(AdditionalOptions)</AdditionalOptions>
577581
<ImportCPaths>third_party/npcap-sdk-1.15/Include/</ImportCPaths>
582+
<Warnings>None</Warnings>
578583
</DCompile>
579584
<Link>
580585
<SubSystem>Console</SubSystem>
@@ -604,6 +609,7 @@
604609
<Main>true</Main>
605610
<EnableDebugMixin>true</EnableDebugMixin>
606611
<AdditionalOptions>-preview=bitfields %(AdditionalOptions)</AdditionalOptions>
612+
<Warnings>None</Warnings>
607613
</DCompile>
608614
<Link>
609615
<SubSystem>Console</SubSystem>
@@ -636,6 +642,7 @@
636642
<BoundsCheck>Off</BoundsCheck>
637643
<EnableDebugMixin>true</EnableDebugMixin>
638644
<AdditionalOptions>-preview=bitfields %(AdditionalOptions)</AdditionalOptions>
645+
<Warnings>None</Warnings>
639646
</DCompile>
640647
<Link>
641648
<SubSystem>Console</SubSystem>

openwatt.vcxproj.filters

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@
8282
<Filter Include="src\apps">
8383
<UniqueIdentifier>{8643bbe8-bb9e-448c-88a7-e8beae4c45e8}</UniqueIdentifier>
8484
</Filter>
85+
<Filter Include="src\apps\api">
86+
<UniqueIdentifier>{8e7eb135-c8b1-4d1c-9b24-0b1b9e1f0f07}</UniqueIdentifier>
87+
</Filter>
8588
<Filter Include="src\apps\energy">
8689
<UniqueIdentifier>{64b88f6a-915f-4210-b838-b7905c8455ad}</UniqueIdentifier>
8790
</Filter>
@@ -378,6 +381,9 @@
378381
<DCompile Include="src\protocol\dns\server.d">
379382
<Filter>src\protocol\dns</Filter>
380383
</DCompile>
384+
<DCompile Include="src\apps\api\package.d">
385+
<Filter>src\apps\api</Filter>
386+
</DCompile>
381387
<DCompile Include="src\apps\energy\appliance.d">
382388
<Filter>src\apps\energy</Filter>
383389
</DCompile>

src/apps/api/package.d

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
module apps.api;
2+
3+
import urt.array;
4+
import urt.format.json;
5+
import urt.lifetime;
6+
import urt.mem.allocator;
7+
import urt.mem.temp;
8+
import urt.string;
9+
import urt.time;
10+
11+
import manager;
12+
import manager.base;
13+
import manager.collection;
14+
import manager.console;
15+
import manager.plugin;
16+
17+
import protocol.http.message;
18+
import protocol.http.server;
19+
20+
import router.stream;
21+
22+
nothrow @nogc:
23+
24+
25+
class APIManager : BaseObject
26+
{
27+
__gshared Property[2] Properties = [ Property.create!("http-server", http_server)(),
28+
Property.create!("uri", uri)() ];
29+
nothrow @nogc:
30+
31+
enum TypeName = StringLit!"api";
32+
33+
this(String name, ObjectFlags flags = ObjectFlags.None)
34+
{
35+
super(collection_type_info!APIManager, name.move, flags);
36+
}
37+
38+
// Properties
39+
40+
inout(HTTPServer) http_server() inout pure
41+
=> _server;
42+
void http_server(HTTPServer value)
43+
{
44+
_server = value;
45+
}
46+
47+
const(char)[] uri() const pure
48+
=> _uri[];
49+
void uri(const(char)[] value)
50+
{
51+
_uri = value.makeString(g_app.allocator);
52+
}
53+
54+
// BaseObject overrides
55+
56+
override bool validate() const pure
57+
=> _server !is null;
58+
59+
override CompletionStatus validating()
60+
{
61+
if (_server.detached)
62+
{
63+
import protocol.http;
64+
if (HTTPServer s = get_module!HTTPModule.servers.get(_server.name))
65+
_server = s;
66+
}
67+
return super.validating();
68+
}
69+
70+
override CompletionStatus startup()
71+
{
72+
if (_uri)
73+
{
74+
_server.add_uri_handler(HTTPMethod.GET, _uri, &handle_request);
75+
_server.add_uri_handler(HTTPMethod.POST, _uri, &handle_request);
76+
_server.add_uri_handler(HTTPMethod.OPTIONS, _uri, &handle_request);
77+
}
78+
else
79+
_default_handler = _server.hook_global_handler(&handle_request);
80+
81+
return CompletionStatus.Complete;
82+
}
83+
84+
override CompletionStatus shutdown()
85+
{
86+
// TODO: need to unlink these things...
87+
return CompletionStatus.Complete;
88+
}
89+
90+
override void update()
91+
{
92+
update_pending_requests();
93+
}
94+
95+
private:
96+
ObjectRef!HTTPServer _server;
97+
String _uri;
98+
99+
HTTPServer.RequestHandler _default_handler;
100+
101+
struct PendingRequest
102+
{
103+
HTTPVersion ver;
104+
Stream stream;
105+
StringSession session;
106+
CommandState command;
107+
}
108+
Array!PendingRequest _pending_requests;
109+
110+
int handle_request(ref const HTTPMessage request, ref Stream stream)
111+
{
112+
const(char)[] tail = request.request_target[_uri.length .. $];
113+
114+
// Handle CORS preflight OPTIONS requests
115+
if (request.method == HTTPMethod.OPTIONS)
116+
return handle_options(request, stream);
117+
118+
if (tail == "/health")
119+
return handle_health(request, stream);
120+
if (tail == "/cli/execute")
121+
return handle_cli_execute(request, stream);
122+
123+
if (_default_handler)
124+
_default_handler(request, stream);
125+
else if (_uri)
126+
{
127+
HTTPMessage response = create_response(request.http_version, 404, StringLit!"Not Found", StringLit!"application/json", "{\"error\":\"Not Found\"}");
128+
stream.write(response.format_message()[]);
129+
}
130+
131+
return 0;
132+
}
133+
134+
void add_cors_headers(ref HTTPMessage response)
135+
{
136+
response.headers ~= HTTPParam(StringLit!"Access-Control-Allow-Origin", StringLit!"*");
137+
response.headers ~= HTTPParam(StringLit!"Access-Control-Allow-Methods", StringLit!"GET, POST, PUT, DELETE, OPTIONS");
138+
response.headers ~= HTTPParam(StringLit!"Access-Control-Allow-Headers", StringLit!"Content-Type");
139+
}
140+
141+
int handle_options(ref const HTTPMessage request, ref Stream stream)
142+
{
143+
HTTPMessage response = create_response(request.http_version, 204, StringLit!"No Content", String(), null);
144+
add_cors_headers(response);
145+
stream.write(response.format_message()[]);
146+
return 0;
147+
}
148+
149+
int handle_health(ref const HTTPMessage request, ref Stream stream)
150+
{
151+
HTTPMessage response = create_response(request.http_version, 200, StringLit!"OK", StringLit!"application/json", tconcat("{\"status\":\"healthy\",\"uptime\":", getAppTime().as!"seconds", "}"));
152+
add_cors_headers(response);
153+
stream.write(response.format_message()[]);
154+
return 0;
155+
}
156+
157+
int handle_cli_execute(ref const HTTPMessage request, ref Stream stream)
158+
{
159+
const(char)[] command_text;
160+
if (request.method == HTTPMethod.GET)
161+
{
162+
command_text = request.param("command")[];
163+
}
164+
else
165+
{
166+
Variant json = parse_json(cast(char[])request.content[]);
167+
command_text = json.getMember("command").asString();
168+
}
169+
170+
if (command_text.length == 0)
171+
{
172+
HTTPMessage response = create_response(request.http_version, 400, StringLit!"Bad Request", StringLit!"application/json", "{\"error\":\"Command body required\"}");
173+
add_cors_headers(response);
174+
stream.write(response.format_message()[]);
175+
return 0;
176+
}
177+
178+
StringSession session = g_app.console.createSession!StringSession();
179+
CommandState cmd = g_app.console.execute(session, command_text);
180+
if (cmd is null)
181+
{
182+
MutableString!0 output = session.takeOutput();
183+
send_cli_response(request.http_version, stream, output[]);
184+
defaultAllocator().freeT(session);
185+
return 0;
186+
}
187+
188+
// TODO: if it's a persistent seession; we need a reference to the session to produce a response.
189+
// if it's an ephemeral session, we need to take the stream from the session so we can produce a deferred response...?
190+
assert(false, "TODO: TEST THIS PATH, I'M NOT SURE THE HTTP REQUEST HANDLER CAN HANDLE HANDLE RELAYED RESPONSE?");
191+
_pending_requests ~= PendingRequest(request.http_version, stream, session, cmd);
192+
return 0;
193+
}
194+
195+
void send_cli_response(HTTPVersion http_version, ref Stream stream, const(char)[] output)
196+
{
197+
HTTPMessage response = create_response(http_version, 200, StringLit!"OK", StringLit!"application/json", "{\"output\":");
198+
if (output.length > 0)
199+
{
200+
import urt.format.json;
201+
202+
const v = Variant(output);
203+
size_t bytes = v.write_json(null);
204+
auto buf = cast(char[])talloc(bytes);
205+
v.write_json(buf);
206+
response.content ~= buf[];
207+
response.content ~= "}";
208+
}
209+
else
210+
response.content ~= "\"\"}";
211+
add_cors_headers(response);
212+
stream.write(response.format_message()[]);
213+
}
214+
215+
void update_pending_requests()
216+
{
217+
size_t i = 0;
218+
while (i < _pending_requests.length)
219+
{
220+
ref PendingRequest req = _pending_requests[i];
221+
222+
if (req.command.update() == CommandCompletionState.InProgress)
223+
{
224+
++i;
225+
continue;
226+
}
227+
228+
MutableString!0 output = req.session.takeOutput();
229+
send_cli_response(req.ver, req.stream, output[]);
230+
231+
defaultAllocator().freeT(req.session);
232+
_pending_requests.remove(i);
233+
}
234+
}
235+
}
236+
237+
238+
class APIModule : Module
239+
{
240+
mixin DeclareModule!"apps.api";
241+
nothrow @nogc:
242+
243+
Collection!APIManager managers;
244+
245+
override void init()
246+
{
247+
g_app.console.registerCollection("/apps/api", managers);
248+
}
249+
250+
override void update()
251+
{
252+
managers.update_all();
253+
}
254+
}

src/manager/console/console.d

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ nothrow @nogc:
232232
pragma(inline, true);
233233

234234
ref Collection!Type* c = collection_for!Type();
235-
assert(c is null, "Collection has been registered before!");
235+
debug assert(c is null, "Collection has been registered before!");
236236
c = &collection;
237237

238238
registerCollectionImpl(_scope, collection);

src/manager/console/session.d

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -656,7 +656,7 @@ nothrow @nogc:
656656
override void writeOutput(const(char)[] text, bool newline)
657657
{
658658
if (newline)
659-
m_output.concat(text, '\n');
659+
m_output.append(text, '\n');
660660
else
661661
m_output ~= text;
662662
}

src/manager/plugin.d

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,9 @@ void register_modules(Application app)
9595
register_module!(protocol.tesla)(app);
9696
register_module!(protocol.zigbee)(app);
9797

98+
import apps.api;
99+
register_module!(apps.api)(app);
100+
98101
import apps.energy;
99102
register_module!(apps.energy)(app);
100103
}

src/manager/system.d

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ void sysinfo(Session session, const(Variant)[] args)
7373
session.writeLine("Processor: ", info.processor);
7474
session.writeLine("Total Memory: ", info.total_memory.format_bytes());
7575
session.writeLine("Available: ", info.available_memory.format_bytes());
76-
session.writeLine("Uptime: ", getAppTime());
76+
session.writeLine("Uptime: ", seconds(getAppTime().as!"seconds"));
7777
}
7878
else foreach (ref arg; args)
7979
{
@@ -95,7 +95,7 @@ void sysinfo(Session session, const(Variant)[] args)
9595
else if (icmp(prop, "available-memory") == 0)
9696
session.writeLine(info.available_memory.format_bytes());
9797
else if (icmp(prop, "uptime") == 0)
98-
session.writeLine(getAppTime());
98+
session.writeLine(seconds(getAppTime().as!"seconds"));
9999
else
100100
session.writeLine("Unknown property: ", prop);
101101
}

0 commit comments

Comments
 (0)