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('ngrokEnabled', FngrokEnabled) then FngrokEnabled := False; if not o.TryGetValue('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.