diff --git a/GomokuAI/Dockerfile b/GomokuAI/Dockerfile index 50d91dd3..e80dd493 100644 --- a/GomokuAI/Dockerfile +++ b/GomokuAI/Dockerfile @@ -8,13 +8,13 @@ RUN apt-get update && apt-get install -y \ cmake \ g++ \ make \ - python3 \ - python3-pip \ + nodejs \ + npm \ && rm -rf /var/lib/apt/lists/* WORKDIR /app/GomokuAI -RUN git clone --recurse-submodules https://github.com/dhbloo/rapfi.git rapfi && \ +RUN git clone --recurse-submodules https://github.com/vkuprin/rapfi.git rapfi && \ cd rapfi/Rapfi && \ mkdir build && cd build && \ cmake .. \ @@ -28,10 +28,9 @@ RUN git clone --recurse-submodules https://github.com/dhbloo/rapfi.git rapfi && -DUSE_NEON_DOTPROD=OFF && \ cmake --build . -RUN pip3 install flask - WORKDIR /app/GomokuAI/rapfi/Rapfi +COPY wrapper.js . -COPY wrapper.py . +RUN npm install express -CMD ["python3", "wrapper.py"] +CMD ["node", "wrapper.js"] diff --git a/GomokuAI/wrapper.js b/GomokuAI/wrapper.js new file mode 100644 index 00000000..45f6f43a --- /dev/null +++ b/GomokuAI/wrapper.js @@ -0,0 +1,87 @@ +const express = require("express"); +const { spawn } = require("child_process"); +const { Writable } = require("stream"); + +const app = express(); +app.use(express.json()); + +const subprocess = spawn("./build/pbrain-rapfi", ["--mode", "gomocup"], { + stdio: ["pipe", "pipe", "pipe"], + shell: true, +}); + +const commandQueue = []; +const responseQueue = []; + +const inputStream = new Writable({ + write(chunk, encoding, callback) { + subprocess.stdin.write(chunk, encoding, callback); + }, +}); + +subprocess.stdout.on("data", (data) => { + const response = data.toString().trim(); + console.log(`[Subprocess Response]: ${response}`); + + if (responseQueue.length > 0) { + const resolve = responseQueue.shift(); + resolve(response); + } +}); + +subprocess.on("error", (err) => { + console.error( + `[Subprocess Error]: Failed to start subprocess - ${err.message}`, + ); +}); + +subprocess.on("exit", (code, signal) => { + console.error( + `[Subprocess Exit]: Exited with code ${code}, signal ${signal}`, + ); +}); + +app.get("/test", (req, res) => { + res.json({ status: "Engine is running" }); +}); + +app.post("/command", async (req, res) => { + const cmd = req.body.command; + if (!cmd) { + return res.status(400).json({ error: "No command provided" }); + } + + console.log(`[Command Sent]: ${cmd}`); + + try { + inputStream.write(cmd + "\n"); + + const response = await new Promise((resolve, reject) => { + const timeout = setTimeout( + () => reject(new Error("Timeout waiting for response")), + 5000, + ); + responseQueue.push((data) => { + clearTimeout(timeout); + resolve(data); + }); + }); + + console.log(`[Response Received]: ${response}`); + res.json({ response }); + } catch (err) { + console.error(`[Command Error]: ${err.message}`); + res.status(408).json({ error: err.message }); + } +}); + +process.on("SIGINT", () => { + console.log("Shutting down server and subprocess..."); + subprocess.kill(); + process.exit(); +}); + +const PORT = 5005; +app.listen(PORT, () => { + console.log(`Server running on http://0.0.0.0:${PORT}`); +}); diff --git a/GomokuAI/wrapper.py b/GomokuAI/wrapper.py deleted file mode 100644 index 29177085..00000000 --- a/GomokuAI/wrapper.py +++ /dev/null @@ -1,52 +0,0 @@ -from flask import Flask, request, jsonify -import subprocess -import threading -import queue - -app = Flask(__name__) - -process = subprocess.Popen( - ['./build/pbrain-rapfi', '--mode', 'gomocup'], - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True -) - -command_queue = queue.Queue() -response_queue = queue.Queue() - -def process_io(): - while True: - try: - command = command_queue.get(timeout=0.1) - process.stdin.write(command + '\n') - process.stdin.flush() - response = process.stdout.readline().strip() - response_queue.put(response) - except queue.Empty: - continue - -io_thread = threading.Thread(target=process_io, daemon=True) -io_thread.start() - -@app.route('/test') -def test(): - return jsonify({"status": "Engine is running"}) - -@app.route('/command', methods=['POST']) -def command(): - cmd = request.json.get('command') - if not cmd: - return jsonify({"error": "No command provided"}), 400 - - command_queue.put(cmd) - - try: - response = response_queue.get(timeout=5) - return jsonify({"response": response}) - except queue.Empty: - return jsonify({"error": "Timeout waiting for response"}), 408 - -if __name__ == '__main__': - app.run(host='0.0.0.0', port=5005) diff --git a/GomokuClient/packages/gomoku-api/client/hooks/index.ts b/GomokuClient/packages/gomoku-api/client/hooks/index.ts index 42e00cc6..d6e6c45e 100644 --- a/GomokuClient/packages/gomoku-api/client/hooks/index.ts +++ b/GomokuClient/packages/gomoku-api/client/hooks/index.ts @@ -5,7 +5,7 @@ export * from "./useGetApiGameRegisteredActive"; export * from "./useGetApiGameRegisteredAvailableToJoin"; export * from "./useGetApiGameRegisteredGameidHistory"; export * from "./useGetApiProfilesUsernameGames"; -export * from "./useGetApiV1RapfiTest"; +export * from "./useGetApiRapfiTest"; export * from "./useGetHealth"; export * from "./usePostApiGameAnonymous"; export * from "./usePostApiGameAnonymousGameidJoin"; diff --git a/GomokuClient/packages/gomoku-api/client/hooks/useGetApiRapfiTest.ts b/GomokuClient/packages/gomoku-api/client/hooks/useGetApiRapfiTest.ts new file mode 100644 index 00000000..aa59fed8 --- /dev/null +++ b/GomokuClient/packages/gomoku-api/client/hooks/useGetApiRapfiTest.ts @@ -0,0 +1,167 @@ +import client from "../../http"; +import { + useQuery, + queryOptions, + useSuspenseQuery, +} from "@tanstack/react-query"; +import type { + GetApiRapfiTestQueryResponse, + GetApiRapfiTestHeaderParams, + GetApiRapfiTest500, +} from "../models/GetApiRapfiTest"; +import type { + QueryObserverOptions, + UseQueryResult, + QueryKey, + UseSuspenseQueryOptions, + UseSuspenseQueryResult, +} from "@tanstack/react-query"; + +type GetApiRapfiTestClient = typeof client< + GetApiRapfiTestQueryResponse, + GetApiRapfiTest500, + never +>; +type GetApiRapfiTest = { + data: GetApiRapfiTestQueryResponse; + error: GetApiRapfiTest500; + request: never; + pathParams: never; + queryParams: never; + headerParams: GetApiRapfiTestHeaderParams; + response: GetApiRapfiTestQueryResponse; + client: { + parameters: Partial[0]>; + return: Awaited>; + }; +}; +export const getApiRapfiTestQueryKey = () => + [{ url: "/api/rapfi/test" }] as const; +export type GetApiRapfiTestQueryKey = ReturnType< + typeof getApiRapfiTestQueryKey +>; +export function getApiRapfiTestQueryOptions( + headers?: GetApiRapfiTest["headerParams"], + options: GetApiRapfiTest["client"]["parameters"] = {}, +) { + const queryKey = getApiRapfiTestQueryKey(); + return queryOptions({ + queryKey, + queryFn: async () => { + const res = await client< + GetApiRapfiTest["data"], + GetApiRapfiTest["error"] + >({ + method: "get", + url: `/api/rapfi/test`, + headers: { ...headers, ...options.headers }, + ...options, + }); + return res.data; + }, + }); +} +/** + * @summary Test connection with Rapfi engine + * @link /api/rapfi/test + */ +export function useGetApiRapfiTest< + TData = GetApiRapfiTest["response"], + TQueryData = GetApiRapfiTest["response"], + TQueryKey extends QueryKey = GetApiRapfiTestQueryKey, +>( + headers?: GetApiRapfiTest["headerParams"], + options: { + query?: Partial< + QueryObserverOptions< + GetApiRapfiTest["response"], + GetApiRapfiTest["error"], + TData, + TQueryData, + TQueryKey + > + >; + client?: GetApiRapfiTest["client"]["parameters"]; + } = {}, +): UseQueryResult & { + queryKey: TQueryKey; +} { + const { query: queryOptions, client: clientOptions = {} } = options ?? {}; + const queryKey = queryOptions?.queryKey ?? getApiRapfiTestQueryKey(); + const query = useQuery({ + ...(getApiRapfiTestQueryOptions( + headers, + clientOptions, + ) as unknown as QueryObserverOptions), + queryKey, + ...(queryOptions as unknown as Omit), + }) as UseQueryResult & { + queryKey: TQueryKey; + }; + query.queryKey = queryKey as TQueryKey; + return query; +} +export const getApiRapfiTestSuspenseQueryKey = () => + [{ url: "/api/rapfi/test" }] as const; +export type GetApiRapfiTestSuspenseQueryKey = ReturnType< + typeof getApiRapfiTestSuspenseQueryKey +>; +export function getApiRapfiTestSuspenseQueryOptions( + headers?: GetApiRapfiTest["headerParams"], + options: GetApiRapfiTest["client"]["parameters"] = {}, +) { + const queryKey = getApiRapfiTestSuspenseQueryKey(); + return queryOptions({ + queryKey, + queryFn: async () => { + const res = await client< + GetApiRapfiTest["data"], + GetApiRapfiTest["error"] + >({ + method: "get", + url: `/api/rapfi/test`, + headers: { ...headers, ...options.headers }, + ...options, + }); + return res.data; + }, + }); +} +/** + * @summary Test connection with Rapfi engine + * @link /api/rapfi/test + */ +export function useGetApiRapfiTestSuspense< + TData = GetApiRapfiTest["response"], + TQueryKey extends QueryKey = GetApiRapfiTestSuspenseQueryKey, +>( + headers?: GetApiRapfiTest["headerParams"], + options: { + query?: Partial< + UseSuspenseQueryOptions< + GetApiRapfiTest["response"], + GetApiRapfiTest["error"], + TData, + TQueryKey + > + >; + client?: GetApiRapfiTest["client"]["parameters"]; + } = {}, +): UseSuspenseQueryResult & { + queryKey: TQueryKey; +} { + const { query: queryOptions, client: clientOptions = {} } = options ?? {}; + const queryKey = queryOptions?.queryKey ?? getApiRapfiTestSuspenseQueryKey(); + const query = useSuspenseQuery({ + ...(getApiRapfiTestSuspenseQueryOptions( + headers, + clientOptions, + ) as unknown as UseSuspenseQueryOptions), + queryKey, + ...(queryOptions as unknown as Omit), + }) as UseSuspenseQueryResult & { + queryKey: TQueryKey; + }; + query.queryKey = queryKey as TQueryKey; + return query; +} diff --git a/GomokuClient/packages/gomoku-api/client/hooks/useGetApiV1RapfiTest.ts b/GomokuClient/packages/gomoku-api/client/hooks/useGetApiV1RapfiTest.ts deleted file mode 100644 index 4660782d..00000000 --- a/GomokuClient/packages/gomoku-api/client/hooks/useGetApiV1RapfiTest.ts +++ /dev/null @@ -1,168 +0,0 @@ -import client from "../../http"; -import { - useQuery, - queryOptions, - useSuspenseQuery, -} from "@tanstack/react-query"; -import type { - GetApiV1RapfiTestQueryResponse, - GetApiV1RapfiTestHeaderParams, - GetApiV1RapfiTest500, -} from "../models/GetApiV1RapfiTest"; -import type { - QueryObserverOptions, - UseQueryResult, - QueryKey, - UseSuspenseQueryOptions, - UseSuspenseQueryResult, -} from "@tanstack/react-query"; - -type GetApiV1RapfiTestClient = typeof client< - GetApiV1RapfiTestQueryResponse, - GetApiV1RapfiTest500, - never ->; -type GetApiV1RapfiTest = { - data: GetApiV1RapfiTestQueryResponse; - error: GetApiV1RapfiTest500; - request: never; - pathParams: never; - queryParams: never; - headerParams: GetApiV1RapfiTestHeaderParams; - response: GetApiV1RapfiTestQueryResponse; - client: { - parameters: Partial[0]>; - return: Awaited>; - }; -}; -export const getApiV1RapfiTestQueryKey = () => - [{ url: "/api/v1/rapfi/test" }] as const; -export type GetApiV1RapfiTestQueryKey = ReturnType< - typeof getApiV1RapfiTestQueryKey ->; -export function getApiV1RapfiTestQueryOptions( - headers?: GetApiV1RapfiTest["headerParams"], - options: GetApiV1RapfiTest["client"]["parameters"] = {}, -) { - const queryKey = getApiV1RapfiTestQueryKey(); - return queryOptions({ - queryKey, - queryFn: async () => { - const res = await client< - GetApiV1RapfiTest["data"], - GetApiV1RapfiTest["error"] - >({ - method: "get", - url: `/api/v1/rapfi/test`, - headers: { ...headers, ...options.headers }, - ...options, - }); - return res.data; - }, - }); -} -/** - * @summary Test connection with Rapfi engine - * @link /api/v1/rapfi/test - */ -export function useGetApiV1RapfiTest< - TData = GetApiV1RapfiTest["response"], - TQueryData = GetApiV1RapfiTest["response"], - TQueryKey extends QueryKey = GetApiV1RapfiTestQueryKey, ->( - headers?: GetApiV1RapfiTest["headerParams"], - options: { - query?: Partial< - QueryObserverOptions< - GetApiV1RapfiTest["response"], - GetApiV1RapfiTest["error"], - TData, - TQueryData, - TQueryKey - > - >; - client?: GetApiV1RapfiTest["client"]["parameters"]; - } = {}, -): UseQueryResult & { - queryKey: TQueryKey; -} { - const { query: queryOptions, client: clientOptions = {} } = options ?? {}; - const queryKey = queryOptions?.queryKey ?? getApiV1RapfiTestQueryKey(); - const query = useQuery({ - ...(getApiV1RapfiTestQueryOptions( - headers, - clientOptions, - ) as unknown as QueryObserverOptions), - queryKey, - ...(queryOptions as unknown as Omit), - }) as UseQueryResult & { - queryKey: TQueryKey; - }; - query.queryKey = queryKey as TQueryKey; - return query; -} -export const getApiV1RapfiTestSuspenseQueryKey = () => - [{ url: "/api/v1/rapfi/test" }] as const; -export type GetApiV1RapfiTestSuspenseQueryKey = ReturnType< - typeof getApiV1RapfiTestSuspenseQueryKey ->; -export function getApiV1RapfiTestSuspenseQueryOptions( - headers?: GetApiV1RapfiTest["headerParams"], - options: GetApiV1RapfiTest["client"]["parameters"] = {}, -) { - const queryKey = getApiV1RapfiTestSuspenseQueryKey(); - return queryOptions({ - queryKey, - queryFn: async () => { - const res = await client< - GetApiV1RapfiTest["data"], - GetApiV1RapfiTest["error"] - >({ - method: "get", - url: `/api/v1/rapfi/test`, - headers: { ...headers, ...options.headers }, - ...options, - }); - return res.data; - }, - }); -} -/** - * @summary Test connection with Rapfi engine - * @link /api/v1/rapfi/test - */ -export function useGetApiV1RapfiTestSuspense< - TData = GetApiV1RapfiTest["response"], - TQueryKey extends QueryKey = GetApiV1RapfiTestSuspenseQueryKey, ->( - headers?: GetApiV1RapfiTest["headerParams"], - options: { - query?: Partial< - UseSuspenseQueryOptions< - GetApiV1RapfiTest["response"], - GetApiV1RapfiTest["error"], - TData, - TQueryKey - > - >; - client?: GetApiV1RapfiTest["client"]["parameters"]; - } = {}, -): UseSuspenseQueryResult & { - queryKey: TQueryKey; -} { - const { query: queryOptions, client: clientOptions = {} } = options ?? {}; - const queryKey = - queryOptions?.queryKey ?? getApiV1RapfiTestSuspenseQueryKey(); - const query = useSuspenseQuery({ - ...(getApiV1RapfiTestSuspenseQueryOptions( - headers, - clientOptions, - ) as unknown as UseSuspenseQueryOptions), - queryKey, - ...(queryOptions as unknown as Omit), - }) as UseSuspenseQueryResult & { - queryKey: TQueryKey; - }; - query.queryKey = queryKey as TQueryKey; - return query; -} diff --git a/GomokuClient/packages/gomoku-api/client/models/GetApiRapfiTest.ts b/GomokuClient/packages/gomoku-api/client/models/GetApiRapfiTest.ts new file mode 100644 index 00000000..9ee31b92 --- /dev/null +++ b/GomokuClient/packages/gomoku-api/client/models/GetApiRapfiTest.ts @@ -0,0 +1,20 @@ +export type GetApiRapfiTestHeaderParams = { + /** + * @type string | undefined + */ + "X-Version"?: string; +}; +/** + * @description Connection successful + */ +export type GetApiRapfiTest200 = any; +/** + * @description Connection failed + */ +export type GetApiRapfiTest500 = any; +export type GetApiRapfiTestQueryResponse = any; +export type GetApiRapfiTestQuery = { + Response: GetApiRapfiTestQueryResponse; + HeaderParams: GetApiRapfiTestHeaderParams; + Errors: GetApiRapfiTest500; +}; diff --git a/GomokuClient/packages/gomoku-api/client/models/GetApiV1RapfiTest.ts b/GomokuClient/packages/gomoku-api/client/models/GetApiV1RapfiTest.ts deleted file mode 100644 index 50cd37d6..00000000 --- a/GomokuClient/packages/gomoku-api/client/models/GetApiV1RapfiTest.ts +++ /dev/null @@ -1,20 +0,0 @@ -export type GetApiV1RapfiTestHeaderParams = { - /** - * @type string | undefined - */ - "X-Version"?: string; -}; -/** - * @description Connection successful - */ -export type GetApiV1RapfiTest200 = any; -/** - * @description Connection failed - */ -export type GetApiV1RapfiTest500 = any; -export type GetApiV1RapfiTestQueryResponse = any; -export type GetApiV1RapfiTestQuery = { - Response: GetApiV1RapfiTestQueryResponse; - HeaderParams: GetApiV1RapfiTestHeaderParams; - Errors: GetApiV1RapfiTest500; -}; diff --git a/GomokuClient/packages/gomoku-api/client/models/index.ts b/GomokuClient/packages/gomoku-api/client/models/index.ts index a66b93ed..c8e0c8a0 100644 --- a/GomokuClient/packages/gomoku-api/client/models/index.ts +++ b/GomokuClient/packages/gomoku-api/client/models/index.ts @@ -12,7 +12,7 @@ export * from "./GetApiGameRegisteredActive"; export * from "./GetApiGameRegisteredAvailableToJoin"; export * from "./GetApiGameRegisteredGameidHistory"; export * from "./GetApiProfilesUsernameGames"; -export * from "./GetApiV1RapfiTest"; +export * from "./GetApiRapfiTest"; export * from "./GetAvailableGamesResponse"; export * from "./GetAvailableGamesResponseIEnumerablePaginatedResponse"; export * from "./GetGameHistoryResponse"; diff --git a/GomokuClient/packages/gomoku-api/schema.json b/GomokuClient/packages/gomoku-api/schema.json index dc17872a..cd5826a3 100644 --- a/GomokuClient/packages/gomoku-api/schema.json +++ b/GomokuClient/packages/gomoku-api/schema.json @@ -319,7 +319,7 @@ } } }, - "/api/v1/rapfi/test": { + "/api/rapfi/test": { "get": { "tags": ["RapfiEngine"], "summary": "Test connection with Rapfi engine", diff --git a/GomokuServer/src/GomokuServer.Api/Controllers/v1/RapfiEngineController.cs b/GomokuServer/src/GomokuServer.Api/Controllers/v1/RapfiEngineController.cs index 258979ec..d5af31af 100644 --- a/GomokuServer/src/GomokuServer.Api/Controllers/v1/RapfiEngineController.cs +++ b/GomokuServer/src/GomokuServer.Api/Controllers/v1/RapfiEngineController.cs @@ -2,7 +2,7 @@ namespace GomokuServer.Api.Controllers.v1; [ApiController] [ApiVersion("1.0")] -[Route("api/v1/rapfi")] +[Route("api/rapfi")] [EnableCors(CorsPolicyName.GomokuClient)] public class RapfiEngineController : Controller {