mirror of
https://github.com/keymanapp/keyman.git
synced 2026-08-07 01:15:33 +00:00
Initial commit for Keyman Developer Server, running on Node. Supports web debugger only at this point. Extensive rewrite of web debugger, much of it more modern standards, significantly improved UX: 1. Drop keyboards, packages, fonts, models onto debug page to load them 2. Websocket for instant reload of recompiled keyboards, packages, models. 3. API for interacting with debug server. 4. Cache of recently used items. 5. Integration with ngrok for public access as desired. 6. Tray icon (currently default icon) for control of server. 7. Server can live after TIKE closes, supports multiple TIKE instances. 8. UI of debug host page rewritten with Bootstrap. 9. Packages downloads integrated into main page, and download link for Keyman supports all platforms (Linux is just a link to instructions...) 10. Avoids file locking and contention by managing all files internally. 11. Not yet tested, but should run cross-platform.
394 lines
10 KiB
ObjectPascal
394 lines
10 KiB
ObjectPascal
unit Keyman.Developer.System.KMDevServerAPI;
|
|
|
|
interface
|
|
|
|
uses
|
|
System.Classes,
|
|
System.Net.HttpClient,
|
|
System.Net.Mime;
|
|
|
|
type
|
|
TKMDevServerDebugAPI = class
|
|
private
|
|
class var
|
|
FStatusChecked: Boolean;
|
|
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;
|
|
public
|
|
class function Running: Boolean; static;
|
|
|
|
class procedure StartServer; static;
|
|
class procedure StopServer; static;
|
|
|
|
class function IsKeyboardRegistered(const Filename: string): Boolean; static;
|
|
class procedure RegisterKeyboard(const Filename, Version, FontFace, OskFontFace: string); static;
|
|
class procedure UnregisterKeyboard(const Filename: string); static;
|
|
|
|
class function IsPackageRegistered(const Filename: string): Boolean; static;
|
|
class procedure RegisterPackage(const Filename, Name: string); static;
|
|
class procedure UnregisterPackage(const Filename: string); static;
|
|
|
|
class function IsModelRegistered(const Filename: string): Boolean; static;
|
|
class procedure RegisterModel(const Filename: string); static;
|
|
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;
|
|
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;
|
|
class property StatusChecked: Boolean read FStatusChecked;
|
|
end;
|
|
|
|
implementation
|
|
|
|
uses
|
|
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;
|
|
|
|
{ TKMDevServerDebugAPI }
|
|
|
|
class procedure TKMDevServerDebugAPI.StartServer;
|
|
var
|
|
pi: TProcessInformation;
|
|
sw: Integer;
|
|
begin
|
|
if Running then
|
|
Exit;
|
|
|
|
// if FKeymanDeveloperOptions.WebHostKeepNGrokControlWindowVisible
|
|
// then sw := SW_SHOWNORMAL
|
|
// else sw := SW_HIDE;
|
|
|
|
sw := SW_SHOWNORMAL;
|
|
|
|
if not TUtilExecute.Execute(Format('"%s" .', [TKeymanDeveloperPaths.NodePath]),
|
|
TKeymanDeveloperPaths.KMDevServerPath, sw, pi) then
|
|
begin
|
|
RaiseLastOSError; //ShowMessage(SysErrorMessage(GetLastError));
|
|
end;
|
|
|
|
FStatusChecked := False;
|
|
end;
|
|
|
|
function QueryFullProcessImageName(
|
|
hProcess: THandle;
|
|
dwFlags: DWORD;
|
|
lpExeName: LPWSTR;
|
|
var lpdwSize: DWORD): BOOL; stdcall; external kernel32
|
|
name 'QueryFullProcessImageNameW';
|
|
|
|
class procedure TKMDevServerDebugAPI.StopServer;
|
|
var
|
|
mfd: TMultipartFormData;
|
|
begin
|
|
mfd := TMultipartFormData.Create;
|
|
try
|
|
Post('shutdown', mfd);
|
|
finally
|
|
mfd.Free;
|
|
end;
|
|
|
|
FStatusChecked := False;
|
|
end;
|
|
|
|
class function TKMDevServerDebugAPI.Running: Boolean;
|
|
var
|
|
s: TStringStream;
|
|
pid: Integer;
|
|
h: THandle;
|
|
buf: array[0..MAX_PATH] of char;
|
|
sz: DWord;
|
|
fs: TFileStream;
|
|
begin
|
|
Result := False;
|
|
|
|
// TODO: check a ping endpoint instead?
|
|
|
|
if not FileExists(TKeymanDeveloperPaths.KMDevServerDataPath + 'lock.json') or
|
|
not FileExists(TKeymanDeveloperPaths.KMDevServerDataPath + 'pid.json') then
|
|
begin
|
|
Exit;
|
|
end;
|
|
|
|
{if System.SysUtils.DeleteFile(TKeymanDeveloperPaths.KMDevServerDataPath + 'lock.json') then
|
|
begin
|
|
// Suggests improper shutdown of node as lock.json should not be deleteable
|
|
// if the server is running
|
|
Exit;
|
|
end;}
|
|
|
|
fs := TFileStream.Create(TKeymanDeveloperPaths.KMDevServerDataPath + 'pid.json', fmOpenRead);
|
|
try
|
|
s := TStringStream.Create('', TEncoding.UTF8);
|
|
try
|
|
s.CopyFrom(fs, 0);
|
|
|
|
if not TryStrToInt(s.DataString.Trim, pid) then
|
|
Exit;
|
|
finally
|
|
s.Free;
|
|
end;
|
|
finally
|
|
fs.Free;
|
|
end;
|
|
|
|
// 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 function TKMDevServerDebugAPI.GetStatus: Boolean;
|
|
var
|
|
http: THttpClient;
|
|
res: IHTTPResponse;
|
|
o: TJSONObject;
|
|
begin
|
|
FngrokEnabled := False;
|
|
FngrokEndpoint := '';
|
|
|
|
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 := '';
|
|
|
|
FStatusChecked := True;
|
|
end;
|
|
except
|
|
Result := False;
|
|
end;
|
|
finally
|
|
http.Free;
|
|
end;
|
|
end;
|
|
|
|
class function TKMDevServerDebugAPI.HostName: string;
|
|
begin
|
|
Result := 'http://localhost:'+FKeymanDeveloperOptions.WebHostDefaultPort.ToString+'/';
|
|
end;
|
|
|
|
class function TKMDevServerDebugAPI.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 TKMDevServerDebugAPI.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 TKMDevServerDebugAPI.IsFontRegistered(
|
|
const Filename: string): Boolean;
|
|
begin
|
|
Result := IsDebugObjectRegistered('font', ExtractFileName(Filename));
|
|
end;
|
|
|
|
class procedure TKMDevServerDebugAPI.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 TKMDevServerDebugAPI.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;
|
|
|
|
class procedure TKMDevServerDebugAPI.UnregisterFont(const Facename: string);
|
|
begin
|
|
|
|
end;
|
|
|
|
//------------------------------------------------------------------------------
|
|
// Keyboard API wrappers
|
|
//------------------------------------------------------------------------------
|
|
|
|
class function TKMDevServerDebugAPI.IsKeyboardRegistered(
|
|
const Filename: string): Boolean;
|
|
begin
|
|
Result := IsDebugObjectRegistered('keyboard', TKeyboardUtils.KeyboardFileNameToID(Filename));
|
|
end;
|
|
|
|
class procedure TKMDevServerDebugAPI.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;
|
|
|
|
class procedure TKMDevServerDebugAPI.UnregisterKeyboard(const Filename: string);
|
|
begin
|
|
|
|
end;
|
|
|
|
//------------------------------------------------------------------------------
|
|
// Model API wrappers
|
|
//------------------------------------------------------------------------------
|
|
|
|
class function TKMDevServerDebugAPI.IsModelRegistered(
|
|
const Filename: string): Boolean;
|
|
begin
|
|
Result := IsDebugObjectRegistered('model', TLexicalModelUtils.LexicalModelFileNameToID(Filename));
|
|
end;
|
|
|
|
class procedure TKMDevServerDebugAPI.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;
|
|
|
|
class procedure TKMDevServerDebugAPI.UnregisterModel(const Filename: string);
|
|
begin
|
|
|
|
end;
|
|
|
|
//------------------------------------------------------------------------------
|
|
// Package API wrappers
|
|
//------------------------------------------------------------------------------
|
|
|
|
class function TKMDevServerDebugAPI.IsPackageRegistered(
|
|
const Filename: string): Boolean;
|
|
begin
|
|
Result := IsDebugObjectRegistered('package', TKeyboardUtils.KeyboardFileNameToID(Filename));
|
|
end;
|
|
|
|
class procedure TKMDevServerDebugAPI.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;
|
|
|
|
class procedure TKMDevServerDebugAPI.UnregisterPackage(const Filename: string);
|
|
begin
|
|
|
|
end;
|
|
|
|
class function TKMDevServerDebugAPI.UpdateStatus: Boolean;
|
|
begin
|
|
if FStatusChecked
|
|
then Result := True
|
|
else Result := GetStatus;
|
|
end;
|
|
|
|
end.
|