mirror of
https://github.com/keymanapp/keyman.git
synced 2026-08-06 08:55:34 +00:00
460 lines
12 KiB
ObjectPascal
460 lines
12 KiB
ObjectPascal
unit Keyman.Developer.System.ServerAPI;
|
|
|
|
interface
|
|
|
|
uses
|
|
System.Classes,
|
|
System.Net.HttpClient,
|
|
System.Net.Mime;
|
|
|
|
type
|
|
TServerDebugAPI = class
|
|
private
|
|
class var
|
|
LastGetStatusTime: UInt64;
|
|
FngrokEnabled: Boolean;
|
|
FngrokEndpoint: string;
|
|
class function IsDebugObjectRegistered(const objectType, id: string): Boolean; static;
|
|
class procedure Post(const api: string; mfd: TMultipartFormData); static;
|
|
class function HostName: string; static;
|
|
class function LoadFileToString(const Filename: string;
|
|
var s: string): Boolean; static;
|
|
public
|
|
// Server Control functions
|
|
class function Running: Boolean; static;
|
|
class procedure StartServer; static;
|
|
class procedure StopServer; static;
|
|
class procedure GetServerURLs(v: TStrings); static;
|
|
|
|
// API endpoints
|
|
class function IsKeyboardRegistered(const Filename: string): Boolean; static;
|
|
class procedure RegisterKeyboard(const Filename, Version, FontFace, OskFontFace: string); static;
|
|
// Not currently used: class procedure UnregisterKeyboard(const Filename: string); static;
|
|
|
|
class function IsPackageRegistered(const Filename: string): Boolean; static;
|
|
class procedure RegisterPackage(const Filename, Name: string); static;
|
|
// Not currently used: class procedure UnregisterPackage(const Filename: string); static;
|
|
|
|
class function IsModelRegistered(const Filename: string): Boolean; static;
|
|
class procedure RegisterModel(const Filename: string); static;
|
|
// Not currently used: class procedure UnregisterModel(const Filename: string); static;
|
|
|
|
class function IsFontRegistered(const Filename: string): Boolean; static;
|
|
class procedure RegisterFont(const Filename, Facename: string); overload; static;
|
|
class procedure RegisterFont(Filedata: TStream; const Facename: string); overload; static;
|
|
// Not currently used: class procedure UnregisterFont(const Facename: string); static;
|
|
|
|
class function GetStatus: Boolean; static;
|
|
class function UpdateStatus: Boolean; static;
|
|
|
|
class property ngrokEnabled: Boolean read FngrokEnabled;
|
|
class property ngrokEndpoint: string read FngrokEndpoint;
|
|
end;
|
|
|
|
implementation
|
|
|
|
uses
|
|
IdGlobal,
|
|
IdGlobalProtocols,
|
|
IdStack,
|
|
System.JSON,
|
|
System.NetEncoding,
|
|
System.Net.URLClient,
|
|
System.SysUtils,
|
|
KeymanDeveloperOptions,
|
|
Keyman.Developer.System.KeymanDeveloperPaths,
|
|
Keyman.System.KeyboardUtils,
|
|
Keyman.System.LexicalModelUtils,
|
|
utilexecute,
|
|
utilsystem,
|
|
Winapi.ShlObj,
|
|
Winapi.Windows;
|
|
|
|
{ TServerDebugAPI }
|
|
|
|
class procedure TServerDebugAPI.StartServer;
|
|
var
|
|
sw: Integer;
|
|
begin
|
|
if Running then
|
|
Exit;
|
|
|
|
if FKeymanDeveloperOptions.ServerServerShowConsoleWindow
|
|
then sw := SW_SHOWNORMAL
|
|
else sw := SW_HIDE;
|
|
|
|
if not TUtilExecute.Execute(Format('"%s" .', [TKeymanDeveloperPaths.NodePath]),
|
|
TKeymanDeveloperPaths.ServerPath, sw) then
|
|
begin
|
|
RaiseLastOSError;
|
|
end;
|
|
|
|
LastGetStatusTime := 0;
|
|
end;
|
|
|
|
function QueryFullProcessImageName(
|
|
hProcess: THandle;
|
|
dwFlags: DWORD;
|
|
lpExeName: LPWSTR;
|
|
var lpdwSize: DWORD): BOOL; stdcall; external kernel32
|
|
name 'QueryFullProcessImageNameW';
|
|
|
|
class procedure TServerDebugAPI.StopServer;
|
|
var
|
|
mfd: TMultipartFormData;
|
|
begin
|
|
mfd := TMultipartFormData.Create;
|
|
try
|
|
Post('shutdown', mfd);
|
|
finally
|
|
mfd.Free;
|
|
end;
|
|
|
|
LastGetStatusTime := 0;
|
|
end;
|
|
|
|
class function TServerDebugAPI.LoadFileToString(const Filename: string; var s: string): Boolean;
|
|
var
|
|
fs: TFileStream;
|
|
ss: TStringStream;
|
|
begin
|
|
try
|
|
fs := TFileStream.Create(Filename, fmOpenRead);
|
|
try
|
|
ss := TStringStream.Create('', TEncoding.UTF8);
|
|
try
|
|
ss.CopyFrom(fs, 0);
|
|
s := ss.DataString;
|
|
Result := True;
|
|
finally
|
|
ss.Free;
|
|
end;
|
|
finally
|
|
fs.Free;
|
|
end;
|
|
except
|
|
on E:EFileStreamError do
|
|
begin
|
|
// TODO: Log this error?
|
|
Result := False;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
class function TServerDebugAPI.Running: Boolean;
|
|
var
|
|
s: string;
|
|
pid: Integer;
|
|
h: THandle;
|
|
buf: array[0..MAX_PATH] of char;
|
|
sz: DWord;
|
|
begin
|
|
Result := False;
|
|
|
|
// TODO: check a ping endpoint instead?
|
|
|
|
if not FileExists(TKeymanDeveloperPaths.ServerDataPath + 'lock.json') or
|
|
not FileExists(TKeymanDeveloperPaths.ServerDataPath + 'pid.json') then
|
|
begin
|
|
Exit;
|
|
end;
|
|
|
|
{if System.SysUtils.DeleteFile(TKeymanDeveloperPaths.ServerDataPath + 'lock.json') then
|
|
begin
|
|
// Suggests improper shutdown of node as lock.json should not be deleteable
|
|
// if the server is running
|
|
Exit;
|
|
end;}
|
|
|
|
if not LoadFileToString(TKeymanDeveloperPaths.ServerDataPath + 'pid.json', s) then
|
|
Exit;
|
|
if not TryStrToInt(s.Trim, pid) then
|
|
Exit;
|
|
|
|
// check the pid, and if it is node, we'll basically assume it's our one
|
|
|
|
h := OpenProcess(PROCESS_ALL_ACCESS, False, pid);
|
|
if h = 0 then
|
|
Exit;
|
|
try
|
|
sz := MAX_PATH;
|
|
if not QueryFullProcessImageName(h, 0, buf, sz) then
|
|
Exit;
|
|
if not SameText(ExtractFileName(buf), 'node.exe') then
|
|
Exit;
|
|
finally
|
|
CloseHandle(h);
|
|
end;
|
|
|
|
Result := True;
|
|
end;
|
|
|
|
class procedure TServerDebugAPI.GetServerURLs(v: TStrings);
|
|
const
|
|
IPv4Loopback = '127.0.0.1'; {do not localize}
|
|
|
|
function GetHostName(tp: TComputerNameFormat): string;
|
|
var
|
|
buf: array[0..260] of char;
|
|
sz: Cardinal;
|
|
begin
|
|
if GetComputerNameEx(tp, buf, sz) then
|
|
Result := buf
|
|
else
|
|
Result := '';
|
|
end;
|
|
var
|
|
port: string;
|
|
sNetbios: string;
|
|
sHost: string;
|
|
sFull: string;
|
|
i: Integer;
|
|
FIPv4Addresses: TIdStackLocalAddressList;
|
|
begin
|
|
if TServerDebugAPI.UpdateStatus and
|
|
(TServerDebugAPI.ngrokEndpoint <> '') then
|
|
begin
|
|
v.Add(TServerDebugAPI.ngrokEndpoint);
|
|
end;
|
|
if FKeymanDeveloperOptions.ServerUseLocalAddresses then
|
|
begin
|
|
port := ':'+IntToStr(FKeymanDeveloperOptions.ServerDefaultPort);
|
|
sFull := GetHostName(ComputerNameDnsFullyQualified);
|
|
sHost := GetHostName(ComputerNameDnsHostname);
|
|
sNetbios := GetHostName(ComputerNameNetBIOS);
|
|
if SameText(sHost, sFull) then sHost := '';
|
|
if SameText(sNetbios, sHost) or SameText(sNetbios, sFull) then sNetbios := '';
|
|
|
|
if sFull <> '' then v.Add('http://'+sFull+port);
|
|
if sHost <> '' then v.Add('http://'+sHost+port);
|
|
if sNetbios <> '' then v.Add('http://'+sNetbios+port);
|
|
|
|
FIPv4Addresses := TIdStackLocalAddressList.Create;
|
|
try
|
|
TIdStack.IncUsage;
|
|
try
|
|
GStack.GetLocalAddressList(FIPv4Addresses);
|
|
finally
|
|
TIdStack.DecUsage;
|
|
end;
|
|
|
|
for i := 0 to FIPv4Addresses.Count - 1 do
|
|
v.Add('http://'+FIPv4Addresses[i].IPAddress+port);
|
|
finally
|
|
FIPv4Addresses.Free;
|
|
end;
|
|
|
|
v.Add('http://localhost'+port);
|
|
v.Add('http://'+IPv4Loopback+port);
|
|
end;
|
|
end;
|
|
|
|
class function TServerDebugAPI.UpdateStatus: Boolean;
|
|
begin
|
|
if (GetTickCount64 - LastGetStatusTime > 1000)
|
|
then Result := GetStatus
|
|
else Result := True;
|
|
end;
|
|
|
|
class function TServerDebugAPI.GetStatus: Boolean;
|
|
var
|
|
http: THttpClient;
|
|
res: IHTTPResponse;
|
|
o: TJSONObject;
|
|
begin
|
|
// TODO: Consider reporting this info by file to save http transaction cost?
|
|
// if not LoadFileToString(TKeymanDeveloperPaths.ServerDataPath + 'status.json', s) then
|
|
// Exit(False);
|
|
|
|
FngrokEnabled := False;
|
|
FngrokEndpoint := '';
|
|
|
|
if not Running then
|
|
Exit(False);
|
|
|
|
http := THttpClient.Create;
|
|
try
|
|
try
|
|
http.ConnectionTimeout := 100;
|
|
res := http.Get(HostName+'api/status');
|
|
Result := res.StatusCode = 200;
|
|
if Result then
|
|
begin
|
|
o := TJSONObject.ParseJSONValue(res.ContentAsString) as TJSONObject;
|
|
if not Assigned(o) then
|
|
Exit(False);
|
|
|
|
if not o.TryGetValue<Boolean>('ngrokEnabled', FngrokEnabled) then FngrokEnabled := False;
|
|
if not o.TryGetValue<string>('ngrokEndpoint', FngrokEndpoint) then FngrokEndpoint := '';
|
|
|
|
LastGetStatusTime := GetTickCount64;
|
|
end;
|
|
except
|
|
Result := False;
|
|
end;
|
|
finally
|
|
http.Free;
|
|
end;
|
|
end;
|
|
|
|
class function TServerDebugAPI.HostName: string;
|
|
begin
|
|
Result := 'http://localhost:'+FKeymanDeveloperOptions.ServerDefaultPort.ToString+'/';
|
|
end;
|
|
|
|
class function TServerDebugAPI.IsDebugObjectRegistered(const objectType, id: string): Boolean;
|
|
var
|
|
http: THttpClient;
|
|
uri: TURI;
|
|
begin
|
|
// TODO: port
|
|
// TODO: refactor address
|
|
uri := TURI.Create(HostName+'api/'+objectType);
|
|
uri.AddParameter('id', id);
|
|
http := THttpClient.Create;
|
|
try
|
|
try
|
|
Result := http.Get(uri.ToString).StatusCode = 200;
|
|
except
|
|
Result := False;
|
|
end;
|
|
finally
|
|
http.Free;
|
|
end;
|
|
end;
|
|
|
|
class procedure TServerDebugAPI.Post(const api: string; mfd: TMultipartFormData);
|
|
var
|
|
http: THttpClient;
|
|
begin
|
|
try
|
|
http := THttpClient.Create;
|
|
try
|
|
http.ConnectionTimeout := 100;
|
|
http.Post(HostName+'api/'+api, mfd);
|
|
finally
|
|
http.Free;
|
|
end;
|
|
except
|
|
;
|
|
end;
|
|
end;
|
|
|
|
//------------------------------------------------------------------------------
|
|
// Font API wrappers
|
|
//------------------------------------------------------------------------------
|
|
|
|
class function TServerDebugAPI.IsFontRegistered(
|
|
const Filename: string): Boolean;
|
|
begin
|
|
Result := IsDebugObjectRegistered('font', ExtractFileName(Filename));
|
|
end;
|
|
|
|
class procedure TServerDebugAPI.RegisterFont(const Filename,
|
|
Facename: string);
|
|
var
|
|
mfd: TMultipartFormData;
|
|
begin
|
|
mfd := TMultipartFormData.Create(True);
|
|
try
|
|
mfd.AddField('id', Facename);
|
|
mfd.AddFile('file', Filename);
|
|
Post('font/register', mfd);
|
|
finally
|
|
mfd.Free;
|
|
end;
|
|
end;
|
|
|
|
class procedure TServerDebugAPI.RegisterFont(Filedata: TStream;
|
|
const Facename: string);
|
|
var
|
|
mfd: TMultipartFormData;
|
|
begin
|
|
mfd := TMultipartFormData.Create(True);
|
|
try
|
|
mfd.AddField('id', Facename);
|
|
mfd.AddStream('file', Filedata, 'myfile.ttf', 'application/octet-stream');
|
|
Post('font/register', mfd);
|
|
finally
|
|
mfd.Free;
|
|
end;
|
|
end;
|
|
|
|
//------------------------------------------------------------------------------
|
|
// Keyboard API wrappers
|
|
//------------------------------------------------------------------------------
|
|
|
|
class function TServerDebugAPI.IsKeyboardRegistered(
|
|
const Filename: string): Boolean;
|
|
begin
|
|
Result := IsDebugObjectRegistered('keyboard', TKeyboardUtils.KeyboardFileNameToID(Filename));
|
|
end;
|
|
|
|
class procedure TServerDebugAPI.RegisterKeyboard(const Filename, Version,
|
|
FontFace, OskFontFace: string);
|
|
var
|
|
mfd: TMultipartFormData;
|
|
begin
|
|
mfd := TMultipartFormData.Create(True);
|
|
try
|
|
mfd.AddField('id', TKeyboardUtils.KeyboardFileNameToID(Filename));
|
|
if FontFace <> '' then mfd.AddField('fontFace', FontFace);
|
|
if OskFontFace <> '' then mfd.AddField('oskFontFace', OskFontFace);
|
|
mfd.AddFile('file', Filename);
|
|
Post('keyboard/register', mfd);
|
|
finally
|
|
mfd.Free;
|
|
end;
|
|
end;
|
|
|
|
//------------------------------------------------------------------------------
|
|
// Model API wrappers
|
|
//------------------------------------------------------------------------------
|
|
|
|
class function TServerDebugAPI.IsModelRegistered(
|
|
const Filename: string): Boolean;
|
|
begin
|
|
Result := IsDebugObjectRegistered('model', TLexicalModelUtils.LexicalModelFileNameToID(Filename));
|
|
end;
|
|
|
|
class procedure TServerDebugAPI.RegisterModel(const Filename: string);
|
|
var
|
|
mfd: TMultipartFormData;
|
|
begin
|
|
mfd := TMultipartFormData.Create(True);
|
|
try
|
|
mfd.AddField('id', TLexicalModelUtils.LexicalModelFileNameToID(Filename));
|
|
mfd.AddFile('file', Filename);
|
|
Post('model/register', mfd);
|
|
finally
|
|
mfd.Free;
|
|
end;
|
|
end;
|
|
|
|
//------------------------------------------------------------------------------
|
|
// Package API wrappers
|
|
//------------------------------------------------------------------------------
|
|
|
|
class function TServerDebugAPI.IsPackageRegistered(
|
|
const Filename: string): Boolean;
|
|
begin
|
|
Result := IsDebugObjectRegistered('package', TKeyboardUtils.KeyboardFileNameToID(Filename));
|
|
end;
|
|
|
|
class procedure TServerDebugAPI.RegisterPackage(const Filename,
|
|
Name: string);
|
|
var
|
|
mfd: TMultipartFormData;
|
|
begin
|
|
mfd := TMultipartFormData.Create(True);
|
|
try
|
|
mfd.AddField('id', TKeyboardUtils.KeyboardFileNameToID(Filename));
|
|
mfd.AddField('name', Name);
|
|
mfd.AddFile('file', Filename);
|
|
Post('package/register', mfd);
|
|
finally
|
|
mfd.Free;
|
|
end;
|
|
end;
|
|
|
|
end.
|