Skip to content

Commit 77a3c7f

Browse files
committed
MacOS: enable all tests
* enable recording, streaming, dual-output tests on MacOS * set CI env var for MacOS github runner for obs_handler.ts * fix warning we cant assign null: `let secondContext: osn.IVideo = null;` * do not invoke `AddVideoContext` on CI builds in enhanced broadcasting tests since these are disabled. * main.yml: upgrade to macos-15 for arm64 ARCH (same OS Intel ARCH is using). * main.yml: upload OBS logs to troubleshoot unit tests. main.yml: set fail-fast to false * turn off fail-fast so the tests can continue running which will fix issue where if macos-15-intel fails, macos-arm64 is cancelled before it can finish running. osn_handler: log time delta for long duration * add warning when signal received took longer then expectedDeadline obs_handler: assign explicit log filename for tests Fix orphaned worker thread: The WorkerSignals::worker keeps polling Query. When a test fails before calling destroy(), the server-side recording gets freed when OBS shuts down, but the orphaned worker thread continues. When the next test file creates a fresh OBS connection, Controller::GetInstance().GetConnection() now returns the new session's connection. The orphaned worker sends Query(oldUID) to the new server, which has no knowledge of that uid → "Recording reference is not valid." Remove source message listener when test ends * stops worker thread properly * test_osn_simple_recording: remove lamda to enable timeout Tests: simple_recording,test_osn_advanced_replayBuffer,test_osn_advanced_recording,dual_output - wrap with try/finally * update any test that starts a worker thread via create() to properly invoke destroy() within a finally block to guanrantee the worker thread is stopped even if the test fails. Prevents orphaned worker threads from invoking Query() every 33ms on the new IPC connection created by other tests. guarantees cleanup even when tests throw so they tests will clean up proactively, and any orphaned workers that slip through will stop themselves rather than polluting subsequent test runs. validate totalSleepMS does not have a negative value When the IPC call takes longer than sleepIntervalMS on an overloaded CI runner), sleepIntervalMS - dur.count() produces a negative result that wraps to a huge size_t value, causing the worker to sleep for an effectively infinite duration. After that, no more signals are ever polled — any test waiting for a signal hits the 30-second timeout. do not share stdout/stderr with parent process On macOS, the server is spawned with posix_spawnp with NULL for file_actions, which means it inherits the parent's stdout/stderr. On Windows this doesn't happen because CreateProcessW uses DETACHED_PROCESS | CREATE_NO_WINDOW, which cuts the server off from the parent's console entirely. obs_handler: add more retryable timeouts for flaky tests advanced-recording fix * Removed extraneous set_video_mix since audio encoders don't have a video mix which produced the "encoder 'track1' is not a video encoder" warning * Reduce defaultVideoContext from 1280x720@60fps to 1280x720@30fps to reduce CPU load on the slow x86_64 CI machine. osn-replay-buffer: invoke output handler to save * makes test more reliable * follows similar pattern used by OBS::ReplayBufferSave
1 parent a5e6bbc commit 77a3c7f

22 files changed

Lines changed: 1581 additions & 1547 deletions

.github/workflows/main.yml

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ jobs:
4242
ReleaseName: release
4343
Architecture: x86_64
4444
- BuildReleases: Release-arm64
45-
image: macos-14
45+
image: macos-15
4646
BuildConfig: RelWithDebInfo
4747
ReleaseName: release
4848
Architecture: arm64
@@ -99,6 +99,7 @@ jobs:
9999
needs: build-macos
100100
runs-on: ${{ matrix.image }}
101101
strategy:
102+
fail-fast: false # Don't cancel the other architecture's tests if one fails, so we can get test results for both.
102103
matrix:
103104
BuildReleases: [Release-x86_64, Release-arm64]
104105
include:
@@ -108,7 +109,7 @@ jobs:
108109
ReleaseName: release
109110
Architecture: x86_64
110111
- BuildReleases: Release-arm64
111-
image: macos-14
112+
image: macos-15
112113
BuildConfig: RelWithDebInfo
113114
ReleaseName: release
114115
Architecture: arm64
@@ -151,6 +152,15 @@ jobs:
151152
OSN_ACCESS_KEY_ID: ${{secrets.AWS_RELEASE_ACCESS_KEY_ID}}
152153
OSN_SECRET_ACCESS_KEY: ${{secrets.AWS_RELEASE_SECRET_ACCESS_KEY}}
153154
RELEASE_NAME: ${{matrix.ReleaseName}}
155+
CI: true
156+
SUPPRESS_STREAMLABS_OBS_LOGS: true # Prevents logs from being printed in the test output, but still generates log files that can be uploaded as artifacts.
157+
- name: Upload OBS logs
158+
if: ${{ always() }}
159+
uses: actions/upload-artifact@v4
160+
with:
161+
name: obs-logs-mac-${{ matrix.Architecture }}
162+
path: tests/osn-tests/osnData/slobs-client/node-obs/logs/
163+
if-no-files-found: ignore
154164
# Run even after test failures so the PR still gets the flaky summary.
155165
- name: Publish flaky test check
156166
if: ${{ always() }}
@@ -181,7 +191,7 @@ jobs:
181191
ReleaseName: release
182192
Architecture: x86_64
183193
- BuildReleases: Release-arm64
184-
image: macos-14
194+
image: macos-15
185195
BuildConfig: RelWithDebInfo
186196
ReleaseName: release
187197
Architecture: arm64
@@ -234,7 +244,7 @@ jobs:
234244
ReleaseName: release
235245
Architecture: x86_64
236246
- BuildReleases: Release-arm64
237-
image: macos-14
247+
image: macos-15
238248
BuildConfig: RelWithDebInfo
239249
ReleaseName: release
240250
Architecture: arm64
@@ -360,6 +370,13 @@ jobs:
360370
OSN_ACCESS_KEY_ID: ${{secrets.AWS_RELEASE_ACCESS_KEY_ID}}
361371
OSN_SECRET_ACCESS_KEY: ${{secrets.AWS_RELEASE_SECRET_ACCESS_KEY}}
362372
RELEASE_NAME: release
373+
- name: Upload OBS logs
374+
if: ${{ always() }}
375+
uses: actions/upload-artifact@v4
376+
with:
377+
name: obs-logs-windows
378+
path: tests/osn-tests/osnData/slobs-client/node-obs/logs/
379+
if-no-files-found: ignore
363380
# Run even after test failures so the PR still gets the flaky summary.
364381
- name: Publish flaky test check
365382
if: ${{ always() }}

obs-studio-client/source/controller.cpp

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,16 @@ std::wstring utfWorkingDir = L"";
4949
#include <wchar.h>
5050
#include <windows.h>
5151
#else
52+
#include <algorithm>
53+
#include <errno.h>
5254
#include <signal.h>
5355
#include <libproc.h>
5456
#include <iostream>
5557
#include <spawn.h>
58+
#include <fcntl.h>
59+
#include <strings.h>
60+
#include <unistd.h>
61+
#include <sys/wait.h>
5662
extern char **environ;
5763
#endif
5864

@@ -292,6 +298,8 @@ std::shared_ptr<ipc::client> Controller::host(const std::string &uri)
292298
return nullptr;
293299
}
294300
#else
301+
const int processTimeoutSeconds = 30;
302+
const int sleepIntervalSeconds = 4;
295303
g_util_osx->setServerWorkingDirectoryPath(workingDirectory);
296304
pid_t pids[2048];
297305
int bytes = proc_listpids(PROC_ALL_PIDS, 0, pids, sizeof(pids));
@@ -301,16 +309,31 @@ std::shared_ptr<ipc::client> Controller::host(const std::string &uri)
301309
int st = proc_pidinfo(pids[i], PROC_PIDTBSDINFO, 0, &proc, PROC_PIDTBSDINFO_SIZE);
302310
if (st == PROC_PIDTBSDINFO_SIZE) {
303311
if (strcmp("obs64", proc.pbi_name) == 0) {
304-
if (pids[i] != 0)
305-
kill(pids[i], SIGKILL);
312+
if (pids[i] != 0 && kill(pids[i], SIGKILL) != 0) {
313+
std::cout << "Warning: could not kill orphaned/former obs64 process" << std::endl;
314+
}
306315
}
307316
}
308317
}
309318

310319
pid_t pid;
311320
std::vector<const char *> argv = {"obs64", uri.c_str(), version.c_str(), serverBinaryPath.c_str(), nullptr};
312321

313-
int ret = posix_spawnp(&pid, serverBinaryPath.c_str(), NULL, NULL, const_cast<char *const *>(argv.data()), environ);
322+
const char *suppressLogsEnv = std::getenv("SUPPRESS_STREAMLABS_OBS_LOGS");
323+
int ret = 0;
324+
if (suppressLogsEnv == nullptr || strcasecmp(suppressLogsEnv, "false") == 0) {
325+
// For development, it can be helpful for process to share stdout/stderr.
326+
ret = posix_spawnp(&pid, serverBinaryPath.c_str(), NULL, NULL, const_cast<char *const *>(argv.data()), environ);
327+
} else {
328+
// Do not send the logs to stdout/stderr.
329+
posix_spawn_file_actions_t file_actions;
330+
posix_spawn_file_actions_init(&file_actions);
331+
posix_spawn_file_actions_addopen(&file_actions, STDOUT_FILENO, "/dev/null", O_WRONLY, 0);
332+
posix_spawn_file_actions_addopen(&file_actions, STDERR_FILENO, "/dev/null", O_WRONLY, 0);
333+
ret = posix_spawnp(&pid, serverBinaryPath.c_str(), &file_actions, NULL, const_cast<char *const *>(argv.data()), environ);
334+
posix_spawn_file_actions_destroy(&file_actions);
335+
}
336+
314337
if (ret != 0) {
315338
std::cerr << "Could not spawn the server at " << serverBinaryPath.c_str() << " with error code: " << ret << std::endl;
316339
return nullptr;

obs-studio-client/source/nodeobs_api.cpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,15 @@ Napi::Value api::OBS_API_initAPI(const Napi::CallbackInfo &info)
3636
std::string language;
3737
std::string version;
3838
std::string crashserverurl;
39+
std::string logFilename;
3940

4041
ASSERT_GET_VALUE(info, info[0], language);
4142
ASSERT_GET_VALUE(info, info[1], path);
4243
ASSERT_GET_VALUE(info, info[2], version);
4344
if (info.Length() > 3)
4445
ASSERT_GET_VALUE(info, info[3], crashserverurl);
46+
if (info.Length() > 4)
47+
ASSERT_GET_VALUE(info, info[4], logFilename);
4548

4649
auto conn = GetConnection(info);
4750
if (!conn)
@@ -50,7 +53,7 @@ Napi::Value api::OBS_API_initAPI(const Napi::CallbackInfo &info)
5053
conn->set_freeze_callback(ipc_freeze_callback, path);
5154

5255
std::vector<ipc::value> response = conn->call_synchronous_helper(
53-
"API", "OBS_API_initAPI", {ipc::value(path), ipc::value(language), ipc::value(version), ipc::value(crashserverurl)});
56+
"API", "OBS_API_initAPI", {ipc::value(path), ipc::value(language), ipc::value(version), ipc::value(crashserverurl), ipc::value(logFilename)});
5457

5558
// The API init method will return a response error + graphical error
5659
// If there is a problem with the IPC the number of responses here will be zero so we must validate the

obs-studio-client/source/nodeobs_autoconfig.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ void autoConfig::worker()
6666
do_sleep:
6767
auto tp_end = std::chrono::high_resolution_clock::now();
6868
auto dur = std::chrono::duration_cast<std::chrono::milliseconds>(tp_end - tp_start);
69-
totalSleepMS = sleepIntervalMS - dur.count();
69+
auto durCount = dur.count();
70+
totalSleepMS = durCount < sleepIntervalMS ? sleepIntervalMS - durCount : 0;
7071
std::this_thread::sleep_for(std::chrono::milliseconds(totalSleepMS));
7172
}
7273
return;

obs-studio-client/source/nodeobs_service.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,8 @@ void service::worker()
348348

349349
auto tp_end = std::chrono::high_resolution_clock::now();
350350
auto dur = std::chrono::duration_cast<std::chrono::milliseconds>(tp_end - tp_start);
351-
totalSleepMS = sleepIntervalMS - dur.count();
351+
auto durCount = dur.count();
352+
totalSleepMS = durCount < sleepIntervalMS ? sleepIntervalMS - durCount : 0;
352353
std::this_thread::sleep_for(std::chrono::milliseconds(totalSleepMS));
353354
}
354355

obs-studio-client/source/worker-signals.hpp

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
******************************************************************************/
1818

1919
#pragma once
20+
#include <iostream>
2021
#include <napi.h>
2122
#include "osn-error.hpp"
2223
#include "utility.hpp"
@@ -88,6 +89,17 @@ class WorkerSignals {
8889
auto conn = Controller::GetInstance().GetConnection();
8990
if (conn) {
9091
std::vector<ipc::value> response = conn->call_synchronous_helper(name, "Query", {ipc::value(refID)});
92+
if (!response.empty()) {
93+
ErrorCode firstError = (ErrorCode)response[0].value_union.ui64;
94+
if (firstError == ErrorCode::InvalidReference) {
95+
// This typically happens if the worker thread is orphaned.
96+
std::string errorMessage = response.size() > 1 ? response[1].value_str : "";
97+
std::cout << "Worker thread exiting due to Invalid reference error encountered: " << errorMessage << std::endl;
98+
isWorkerRunning = false;
99+
workerStop = true;
100+
break;
101+
}
102+
}
91103
if ((response.size() == 5) && signalsList.size() < maximum_signals_in_queue) {
92104
ErrorCode error = (ErrorCode)response[0].value_union.ui64;
93105
if (error == ErrorCode::Ok) {
@@ -122,7 +134,8 @@ class WorkerSignals {
122134

123135
auto tp_end = std::chrono::high_resolution_clock::now();
124136
auto dur = std::chrono::duration_cast<std::chrono::milliseconds>(tp_end - tp_start);
125-
totalSleepMS = sleepIntervalMS - dur.count();
137+
auto durCount = dur.count();
138+
totalSleepMS = durCount < sleepIntervalMS ? sleepIntervalMS - durCount : 0;
126139
std::this_thread::sleep_for(std::chrono::milliseconds(totalSleepMS));
127140
}
128141

@@ -140,4 +153,4 @@ class WorkerSignals {
140153
workerThread->join();
141154
}
142155
}
143-
};
156+
};

obs-studio-server/source/nodeobs_api.cpp

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -835,6 +835,17 @@ void addModulePaths()
835835
#endif
836836
}
837837

838+
std::filesystem::path sanitize_path(const std::filesystem::path &input)
839+
{
840+
std::filesystem::path normalized = input.lexically_normal();
841+
842+
if (normalized.is_absolute() || normalized.string().find("..") != std::string::npos) {
843+
return {};
844+
}
845+
846+
return normalized;
847+
}
848+
838849
static void listEncoders(obs_encoder_type type)
839850
{
840851
constexpr uint32_t hide_flags = OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL;
@@ -874,10 +885,20 @@ void OBS_API::OBS_API_initAPI(void *data, const int64_t id, const std::vector<ip
874885
std::string appdata = args[0].value_str;
875886
std::string locale = args[1].value_str;
876887
currentVersion = args[2].value_str;
888+
std::string logFilename;
889+
// Skip index 3 which is reserved for crash-handler server
890+
if (args.size() > 4) {
891+
std::string logname = sanitize_path(args[4].value_str).string();
892+
if (logname.size() > 0) {
893+
std::ostringstream ss;
894+
ss << logname << '-' << GenerateTimeDateFilename("txt");
895+
logFilename = ss.str();
896+
}
897+
}
877898
utility::osn_current_version(currentVersion);
878899

879900
/* Logging */
880-
std::string filename = GenerateTimeDateFilename("txt");
901+
std::string filename = logFilename.size() > 0 ? logFilename : GenerateTimeDateFilename("txt");
881902
std::string log_path = appdata;
882903
log_path.append("/node-obs/logs/");
883904

obs-studio-server/source/osn-replay-buffer.cpp

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -152,19 +152,26 @@ void osn::IReplayBuffer::Query(void *data, const int64_t id, const std::vector<i
152152

153153
void osn::IReplayBuffer::Save(void *data, const int64_t id, const std::vector<ipc::value> &args, std::vector<ipc::value> &rval)
154154
{
155-
obs_enum_hotkeys(
156-
[](void *data, obs_hotkey_id id, obs_hotkey_t *key) {
157-
if (obs_hotkey_get_registerer_type(key) == OBS_HOTKEY_REGISTERER_OUTPUT) {
158-
std::string key_name = obs_hotkey_get_name(key);
159-
if (key_name.compare("ReplayBuffer.Save") == 0) {
160-
obs_hotkey_enable_callback_rerouting(true);
161-
obs_hotkey_trigger_routed_callback(id, true);
162-
}
163-
}
164-
return true;
165-
},
166-
nullptr);
155+
if (args.size() != 1) {
156+
PRETTY_ERROR_RETURN(ErrorCode::Error, "ReplayBuffer::Save received invalid number of arguments.");
157+
}
158+
ReplayBuffer *replayBuffer = static_cast<ReplayBuffer *>(osn::IFileOutput::Manager::GetInstance().find(args[0].value_union.ui64));
159+
if (!replayBuffer) {
160+
PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "ReplayBuffer reference is not valid.");
161+
}
167162

163+
obs_output_t *output = replayBuffer->GetOutput();
164+
if (!output) {
165+
PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Invalid replay buffer output.");
166+
}
167+
168+
calldata_t cd = {0};
169+
proc_handler_t *ph = obs_output_get_proc_handler(output);
170+
bool hasInvoked = proc_handler_call(ph, "save", &cd);
171+
calldata_free(&cd);
172+
173+
if (!hasInvoked)
174+
PRETTY_ERROR_RETURN(ErrorCode::NotFound, "Could not find ReplayBuffer::Save");
168175
rval.push_back(ipc::value((uint64_t)ErrorCode::Ok));
169176
AUTO_DEBUG;
170-
}
177+
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
"local:build": "cmake --build build --target install --config Debug",
2323
"local:clean": "rm -rf build/*",
2424
"test": "electron-mocha -t 80000 --js-flags=\"--expose-gc\" --color -r ts-node/register tests/osn-tests/src/**/*.ts --reporter tests/osn-tests/util/list-reporter.js",
25-
"test:ci": "yarn run test --retries 2"
25+
"test:ci": "yarn run test --retries 3"
2626
},
2727
"devDependencies": {
2828
"@aws-sdk/client-s3": "^3.0.0",

tests/osn-tests/src/test_nodeobs_autoconfig.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import { deleteConfigFiles } from '../util/general';
99
const testName = 'nodeobs_autoconfig';
1010

1111
describe(testName, function() {
12-
this.timeout(30000)
1312
let obs: OBSHandler;
1413
let hasTestFailed: boolean = false;
1514

@@ -50,9 +49,6 @@ describe(testName, function() {
5049
});
5150

5251
it('Run autoconfig', async function() {
53-
if (obs.isDarwin()) {
54-
this.skip();
55-
}
5652
const start = performance.now();
5753
let progressInfo: IConfigProgress;
5854
let settingValue: any;

0 commit comments

Comments
 (0)