shellcommand_ut.cpp 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492
  1. #include "shellcommand.h"
  2. #include "compat.h"
  3. #include "defaults.h"
  4. #include "fs.h"
  5. #include "sigset.h"
  6. #include "spinlock.h"
  7. #include <library/cpp/testing/unittest/env.h>
  8. #include <library/cpp/testing/unittest/registar.h>
  9. #include <util/random/random.h>
  10. #include <util/stream/file.h>
  11. #include <util/stream/str.h>
  12. #include <util/stream/mem.h>
  13. #include <util/string/strip.h>
  14. #include <util/folder/tempdir.h>
  15. #if defined(_win_)
  16. #define NL "\r\n"
  17. const char catCommand[] = "sort"; // not really cat but ok
  18. const size_t textSize = 1;
  19. #else
  20. #define NL "\n"
  21. const char catCommand[] = "/bin/cat";
  22. const size_t textSize = 20000;
  23. #endif
  24. class TGuardedStringStream: public IInputStream, public IOutputStream {
  25. public:
  26. TGuardedStringStream() {
  27. Stream_.Reserve(100);
  28. }
  29. TString Str() const {
  30. with_lock (Lock_) {
  31. return Stream_.Str();
  32. }
  33. return TString(); // line for compiler
  34. }
  35. protected:
  36. size_t DoRead(void* buf, size_t len) override {
  37. with_lock (Lock_) {
  38. return Stream_.Read(buf, len);
  39. }
  40. return 0; // line for compiler
  41. }
  42. void DoWrite(const void* buf, size_t len) override {
  43. with_lock (Lock_) {
  44. return Stream_.Write(buf, len);
  45. }
  46. }
  47. private:
  48. TAdaptiveLock Lock_;
  49. TStringStream Stream_;
  50. };
  51. Y_UNIT_TEST_SUITE(TShellQuoteTest) {
  52. Y_UNIT_TEST(TestQuoteArg) {
  53. TString cmd;
  54. ShellQuoteArg(cmd, "/pr f/krev/prev.exe");
  55. ShellQuoteArgSp(cmd, "-DVal=\"W Quotes\"");
  56. ShellQuoteArgSp(cmd, "-DVal=W Space");
  57. ShellQuoteArgSp(cmd, "-DVal=Blah");
  58. UNIT_ASSERT_STRINGS_EQUAL(cmd, "\"/pr f/krev/prev.exe\" \"-DVal=\\\"W Quotes\\\"\" \"-DVal=W Space\" \"-DVal=Blah\"");
  59. }
  60. } // Y_UNIT_TEST_SUITE(TShellQuoteTest)
  61. Y_UNIT_TEST_SUITE(TShellCommandTest) {
  62. Y_UNIT_TEST(TestNoQuotes) {
  63. TShellCommandOptions options;
  64. options.SetQuoteArguments(false);
  65. TShellCommand cmd("echo hello");
  66. cmd.Run();
  67. UNIT_ASSERT_VALUES_EQUAL(cmd.GetError(), "");
  68. UNIT_ASSERT_VALUES_EQUAL(cmd.GetOutput(), "hello" NL);
  69. UNIT_ASSERT(TShellCommand::SHELL_FINISHED == cmd.GetStatus());
  70. UNIT_ASSERT(cmd.GetExitCode().Defined() && 0 == cmd.GetExitCode());
  71. UNIT_ASSERT_VALUES_EQUAL(cmd.GetQuotedCommand(), "echo hello");
  72. }
  73. Y_UNIT_TEST(TestOnlyNecessaryQuotes) {
  74. TShellCommandOptions options;
  75. options.SetQuoteArguments(true);
  76. TShellCommand cmd("echo");
  77. cmd << "hey"
  78. << "hello&world";
  79. cmd.Run();
  80. UNIT_ASSERT_VALUES_EQUAL(cmd.GetError(), "");
  81. UNIT_ASSERT_VALUES_EQUAL(cmd.GetOutput(), "hey hello&world" NL);
  82. UNIT_ASSERT(TShellCommand::SHELL_FINISHED == cmd.GetStatus());
  83. UNIT_ASSERT(cmd.GetExitCode().Defined() && 0 == cmd.GetExitCode());
  84. }
  85. Y_UNIT_TEST(TestRun) {
  86. TShellCommand cmd("echo");
  87. cmd << "hello";
  88. cmd.Run();
  89. UNIT_ASSERT_VALUES_EQUAL(cmd.GetError(), "");
  90. #if defined(_win_)
  91. UNIT_ASSERT_VALUES_EQUAL(cmd.GetOutput(), "\"hello\"\r\n");
  92. #else
  93. UNIT_ASSERT_VALUES_EQUAL(cmd.GetOutput(), "hello\n");
  94. #endif
  95. UNIT_ASSERT(TShellCommand::SHELL_FINISHED == cmd.GetStatus());
  96. UNIT_ASSERT(cmd.GetExitCode().Defined() && 0 == cmd.GetExitCode());
  97. }
  98. // running with no shell is not implemented for win
  99. // there should be no problem with it as long as SearchPath is on
  100. Y_UNIT_TEST(TestNoShell) {
  101. #if defined(_win_)
  102. const char dir[] = "dir";
  103. #else
  104. const char dir[] = "ls";
  105. #endif
  106. TShellCommandOptions options;
  107. options.SetQuoteArguments(false);
  108. {
  109. options.SetUseShell(false);
  110. TShellCommand cmd(dir, options);
  111. cmd << "|"
  112. << "sort";
  113. cmd.Run();
  114. UNIT_ASSERT(TShellCommand::SHELL_ERROR == cmd.GetStatus());
  115. UNIT_ASSERT(cmd.GetExitCode().Defined() && 0 != cmd.GetExitCode());
  116. }
  117. {
  118. options.SetUseShell(true);
  119. TShellCommand cmd(dir, options);
  120. cmd << "|"
  121. << "sort";
  122. cmd.Run();
  123. UNIT_ASSERT(TShellCommand::SHELL_FINISHED == cmd.GetStatus());
  124. UNIT_ASSERT_VALUES_EQUAL(cmd.GetError().size(), 0u);
  125. UNIT_ASSERT(cmd.GetExitCode().Defined() && 0 == cmd.GetExitCode());
  126. }
  127. }
  128. Y_UNIT_TEST(TestAsyncRun) {
  129. TShellCommandOptions options;
  130. options.SetAsync(true);
  131. #if defined(_win_)
  132. // fails with weird error "Input redirection is not supported"
  133. // TShellCommand cmd("sleep", options);
  134. // cmd << "3";
  135. TShellCommand cmd("ping 1.1.1.1 -n 1 -w 2000", options);
  136. #else
  137. TShellCommand cmd("sleep", options);
  138. cmd << "2";
  139. #endif
  140. UNIT_ASSERT(TShellCommand::SHELL_NONE == cmd.GetStatus());
  141. cmd.Run();
  142. sleep(1);
  143. UNIT_ASSERT(TShellCommand::SHELL_RUNNING == cmd.GetStatus());
  144. cmd.Wait();
  145. UNIT_ASSERT(TShellCommand::SHELL_RUNNING != cmd.GetStatus());
  146. UNIT_ASSERT_VALUES_EQUAL(cmd.GetError(), "");
  147. #if !defined(_win_)
  148. UNIT_ASSERT(TShellCommand::SHELL_FINISHED == cmd.GetStatus());
  149. UNIT_ASSERT_VALUES_EQUAL(cmd.GetOutput().size(), 0u);
  150. UNIT_ASSERT(cmd.GetExitCode().Defined() && 0 == cmd.GetExitCode());
  151. #endif
  152. }
  153. Y_UNIT_TEST(TestQuotes) {
  154. TShellCommandOptions options;
  155. TString input = TString("a\"a a");
  156. TString output;
  157. TStringOutput outputStream(output);
  158. options.SetOutputStream(&outputStream);
  159. TShellCommand cmd("echo", options);
  160. cmd << input;
  161. cmd.Run().Wait();
  162. output = StripString(output);
  163. #if defined(_win_)
  164. UNIT_ASSERT_VALUES_EQUAL("\"a\\\"a a\"", output);
  165. #else
  166. UNIT_ASSERT_VALUES_EQUAL(input, output);
  167. #endif
  168. UNIT_ASSERT_VALUES_EQUAL(cmd.GetError().size(), 0u);
  169. }
  170. Y_UNIT_TEST(TestRunNonexistent) {
  171. TShellCommand cmd("iwerognweiofnewio"); // some nonexistent command name
  172. cmd.Run().Wait();
  173. UNIT_ASSERT(TShellCommand::SHELL_ERROR == cmd.GetStatus());
  174. UNIT_ASSERT_VALUES_UNEQUAL(cmd.GetError().size(), 0u);
  175. UNIT_ASSERT(cmd.GetExitCode().Defined() && 0 != cmd.GetExitCode());
  176. }
  177. Y_UNIT_TEST(TestExitCode) {
  178. TShellCommand cmd("grep qwerty qwerty"); // some nonexistent file name
  179. cmd.Run().Wait();
  180. UNIT_ASSERT(TShellCommand::SHELL_ERROR == cmd.GetStatus());
  181. UNIT_ASSERT_VALUES_UNEQUAL(cmd.GetError().size(), 0u);
  182. UNIT_ASSERT(cmd.GetExitCode().Defined() && 2 == cmd.GetExitCode());
  183. }
  184. // 'type con' and 'copy con con' want real console, not stdin, use sort
  185. Y_UNIT_TEST(TestInput) {
  186. TShellCommandOptions options;
  187. TString input = (TString("a") * 2000).append(NL) * textSize;
  188. TStringInput inputStream(input);
  189. options.SetInputStream(&inputStream);
  190. TShellCommand cmd(catCommand, options);
  191. cmd.Run().Wait();
  192. UNIT_ASSERT_VALUES_EQUAL(input, cmd.GetOutput());
  193. UNIT_ASSERT_VALUES_EQUAL(cmd.GetError().size(), 0u);
  194. }
  195. Y_UNIT_TEST(TestOutput) {
  196. TShellCommandOptions options;
  197. TString input = (TString("a") * 2000).append(NL) * textSize;
  198. TStringInput inputStream(input);
  199. options.SetInputStream(&inputStream);
  200. TString output;
  201. TStringOutput outputStream(output);
  202. options.SetOutputStream(&outputStream);
  203. TShellCommand cmd(catCommand, options);
  204. cmd.Run().Wait();
  205. UNIT_ASSERT_VALUES_EQUAL(input, output);
  206. UNIT_ASSERT_VALUES_EQUAL(cmd.GetError().size(), 0u);
  207. }
  208. Y_UNIT_TEST(TestIO) {
  209. // descriptive test: use all options
  210. TShellCommandOptions options;
  211. options.SetAsync(true);
  212. options.SetQuoteArguments(false);
  213. options.SetLatency(10);
  214. options.SetClearSignalMask(true);
  215. options.SetCloseAllFdsOnExec(true);
  216. options.SetCloseInput(false);
  217. TGuardedStringStream write;
  218. options.SetInputStream(&write);
  219. TGuardedStringStream read;
  220. options.SetOutputStream(&read);
  221. options.SetUseShell(true);
  222. TShellCommand cmd("cat", options);
  223. cmd.Run();
  224. write << "alpha" << NL;
  225. while (read.Str() != "alpha" NL) {
  226. Sleep(TDuration::MilliSeconds(10));
  227. }
  228. write << "omega" << NL;
  229. while (read.Str() != "alpha" NL "omega" NL) {
  230. Sleep(TDuration::MilliSeconds(10));
  231. }
  232. write << "zeta" << NL;
  233. cmd.CloseInput();
  234. cmd.Wait();
  235. UNIT_ASSERT_VALUES_EQUAL(cmd.GetError(), "");
  236. UNIT_ASSERT(TShellCommand::SHELL_FINISHED == cmd.GetStatus());
  237. UNIT_ASSERT_VALUES_EQUAL(read.Str(), "alpha" NL "omega" NL "zeta" NL);
  238. UNIT_ASSERT(cmd.GetExitCode().Defined() && 0 == cmd.GetExitCode());
  239. }
  240. Y_UNIT_TEST(TestStreamClose) {
  241. struct TStream: public IOutputStream {
  242. size_t NumCloses = 0;
  243. void DoWrite(const void* buf, size_t len) override {
  244. Y_UNUSED(buf);
  245. Y_UNUSED(len);
  246. }
  247. void DoFinish() override {
  248. ++NumCloses;
  249. }
  250. } stream;
  251. auto options1 = TShellCommandOptions().SetCloseStreams(false).SetOutputStream(&stream).SetErrorStream(&stream);
  252. TShellCommand("echo hello", options1).Run().Wait();
  253. UNIT_ASSERT_VALUES_EQUAL(stream.NumCloses, 0);
  254. auto options = TShellCommandOptions().SetCloseStreams(true).SetOutputStream(&stream).SetErrorStream(&stream);
  255. TShellCommand("echo hello", options).Run().Wait();
  256. UNIT_ASSERT_VALUES_EQUAL(stream.NumCloses, 2);
  257. }
  258. Y_UNIT_TEST(TestInterruptSimple) {
  259. TShellCommandOptions options;
  260. options.SetAsync(true);
  261. options.SetCloseInput(false);
  262. TGuardedStringStream write;
  263. options.SetInputStream(&write); // set input stream that will be waited by cat
  264. TShellCommand cmd(catCommand, options);
  265. cmd.Run();
  266. sleep(1);
  267. UNIT_ASSERT(TShellCommand::SHELL_RUNNING == cmd.GetStatus());
  268. cmd.Terminate();
  269. cmd.Wait();
  270. UNIT_ASSERT(TShellCommand::SHELL_RUNNING != cmd.GetStatus());
  271. }
  272. #if !defined(_win_)
  273. // this ut is unix-only, port to win using %TEMP%
  274. Y_UNIT_TEST(TestInterrupt) {
  275. TString tmpfile = TString("shellcommand_ut.interrupt.") + ToString(RandomNumber<ui32>());
  276. TShellCommandOptions options;
  277. options.SetAsync(true);
  278. options.SetQuoteArguments(false);
  279. {
  280. TShellCommand cmd("/bin/sleep", options);
  281. cmd << " 1300 & wait; /usr/bin/touch " << tmpfile;
  282. cmd.Run();
  283. sleep(1);
  284. UNIT_ASSERT(TShellCommand::SHELL_RUNNING == cmd.GetStatus());
  285. // Async mode requires Terminate() + Wait() to send kill to child proc!
  286. cmd.Terminate();
  287. cmd.Wait();
  288. UNIT_ASSERT(TShellCommand::SHELL_ERROR == cmd.GetStatus());
  289. UNIT_ASSERT(cmd.GetExitCode().Defined() && -15 == cmd.GetExitCode());
  290. }
  291. sleep(1);
  292. UNIT_ASSERT(!NFs::Exists(tmpfile));
  293. }
  294. // this ut is unix-only (win has no signal mask)
  295. Y_UNIT_TEST(TestSignalMask) {
  296. // block SIGTERM
  297. int rc;
  298. sigset_t newmask, oldmask;
  299. SigEmptySet(&newmask);
  300. SigAddSet(&newmask, SIGTERM);
  301. rc = SigProcMask(SIG_SETMASK, &newmask, &oldmask);
  302. UNIT_ASSERT(rc == 0);
  303. TString tmpfile = TString("shellcommand_ut.interrupt.") + ToString(RandomNumber<ui32>());
  304. TShellCommandOptions options;
  305. options.SetAsync(true);
  306. options.SetQuoteArguments(false);
  307. // child proc should not receive SIGTERM anymore
  308. {
  309. TShellCommand cmd("/bin/sleep", options);
  310. // touch file only if sleep not interrupted by SIGTERM
  311. cmd << " 10 & wait; [ $? == 0 ] || /usr/bin/touch " << tmpfile;
  312. cmd.Run();
  313. sleep(1);
  314. UNIT_ASSERT(TShellCommand::SHELL_RUNNING == cmd.GetStatus());
  315. cmd.Terminate();
  316. cmd.Wait();
  317. UNIT_ASSERT(TShellCommand::SHELL_ERROR == cmd.GetStatus() || TShellCommand::SHELL_FINISHED == cmd.GetStatus());
  318. }
  319. sleep(1);
  320. UNIT_ASSERT(!NFs::Exists(tmpfile));
  321. // child proc should receive SIGTERM
  322. options.SetClearSignalMask(true);
  323. {
  324. TShellCommand cmd("/bin/sleep", options);
  325. // touch file regardless -- it will be interrupted
  326. cmd << " 10 & wait; /usr/bin/touch " << tmpfile;
  327. cmd.Run();
  328. sleep(1);
  329. UNIT_ASSERT(TShellCommand::SHELL_RUNNING == cmd.GetStatus());
  330. cmd.Terminate();
  331. cmd.Wait();
  332. UNIT_ASSERT(TShellCommand::SHELL_ERROR == cmd.GetStatus());
  333. }
  334. sleep(1);
  335. UNIT_ASSERT(!NFs::Exists(tmpfile));
  336. // restore signal mask
  337. rc = SigProcMask(SIG_SETMASK, &oldmask, nullptr);
  338. UNIT_ASSERT(rc == 0);
  339. }
  340. #else
  341. // This ut is windows-only
  342. Y_UNIT_TEST(TestStdinProperlyConstructed) {
  343. TShellCommandOptions options;
  344. options.SetErrorStream(&Cerr);
  345. TShellCommand cmd(BinaryPath("util/system/ut/stdin_osfhandle/stdin_osfhandle"), options);
  346. cmd.Run().Wait();
  347. UNIT_ASSERT(TShellCommand::SHELL_FINISHED == cmd.GetStatus());
  348. UNIT_ASSERT(cmd.GetExitCode().Defined() && 0 == cmd.GetExitCode());
  349. }
  350. #endif
  351. Y_UNIT_TEST(TestInternalError) {
  352. TString input = (TString("a") * 2000).append("\n");
  353. TStringInput inputStream(input);
  354. TMemoryOutput outputStream(nullptr, 0);
  355. TShellCommandOptions options;
  356. options.SetInputStream(&inputStream);
  357. options.SetOutputStream(&outputStream);
  358. TShellCommand cmd(catCommand, options);
  359. cmd.Run().Wait();
  360. UNIT_ASSERT(TShellCommand::SHELL_INTERNAL_ERROR == cmd.GetStatus());
  361. UNIT_ASSERT_VALUES_UNEQUAL(cmd.GetInternalError().size(), 0u);
  362. }
  363. Y_UNIT_TEST(TestHugeOutput) {
  364. TShellCommandOptions options;
  365. TGuardedStringStream stream;
  366. options.SetOutputStream(&stream);
  367. options.SetUseShell(true);
  368. TString input = TString(7000, 'a');
  369. TString command = TStringBuilder{} << "echo " << input;
  370. TShellCommand cmd(command, options);
  371. cmd.Run().Wait();
  372. UNIT_ASSERT_VALUES_EQUAL(stream.Str(), input + NL);
  373. }
  374. Y_UNIT_TEST(TestHugeError) {
  375. TShellCommandOptions options;
  376. TGuardedStringStream stream;
  377. options.SetErrorStream(&stream);
  378. options.SetUseShell(true);
  379. TString input = TString(7000, 'a');
  380. TString command = TStringBuilder{} << "echo " << input << ">&2";
  381. TShellCommand cmd(command, options);
  382. cmd.Run().Wait();
  383. UNIT_ASSERT_VALUES_EQUAL(stream.Str(), input + NL);
  384. }
  385. Y_UNIT_TEST(TestPipeInput) {
  386. TShellCommandOptions options;
  387. options.SetAsync(true);
  388. options.PipeInput();
  389. TShellCommand cmd(catCommand, options);
  390. cmd.Run();
  391. {
  392. TFile file(cmd.GetInputHandle().Release());
  393. TUnbufferedFileOutput fo(file);
  394. fo << "hello" << Endl;
  395. }
  396. cmd.Wait();
  397. UNIT_ASSERT_VALUES_EQUAL(cmd.GetOutput(), "hello" NL);
  398. UNIT_ASSERT_VALUES_EQUAL(cmd.GetError().size(), 0u);
  399. }
  400. Y_UNIT_TEST(TestPipeOutput) {
  401. TShellCommandOptions options;
  402. options.SetAsync(true);
  403. options.PipeOutput();
  404. constexpr TStringBuf firstMessage = "first message";
  405. constexpr TStringBuf secondMessage = "second message";
  406. const TString command = TStringBuilder() << "echo '" << firstMessage << "' && sleep 10 && echo '" << secondMessage << "'";
  407. TShellCommand cmd(command, options);
  408. cmd.Run();
  409. TUnbufferedFileInput cmdOutput(TFile(cmd.GetOutputHandle().Release()));
  410. TString firstLineOutput, secondLineOutput;
  411. {
  412. Sleep(TDuration::Seconds(5));
  413. firstLineOutput = cmdOutput.ReadLine();
  414. cmd.Wait();
  415. secondLineOutput = cmdOutput.ReadLine();
  416. }
  417. UNIT_ASSERT_VALUES_EQUAL(firstLineOutput, firstMessage);
  418. UNIT_ASSERT_VALUES_EQUAL(secondLineOutput, secondLineOutput);
  419. }
  420. Y_UNIT_TEST(TestOptionsConsistency) {
  421. TShellCommandOptions options;
  422. options.SetInheritOutput(false).SetInheritError(false);
  423. options.SetOutputStream(nullptr).SetErrorStream(nullptr);
  424. UNIT_ASSERT(options.OutputMode == TShellCommandOptions::HANDLE_STREAM);
  425. UNIT_ASSERT(options.ErrorMode == TShellCommandOptions::HANDLE_STREAM);
  426. }
  427. Y_UNIT_TEST(TestForkCallback) {
  428. TString tmpFile = TString("shellcommand_ut.test_for_callback.txt");
  429. TFsPath cwd(::NFs::CurrentWorkingDirectory());
  430. const TString tmpFilePath = cwd.Child(tmpFile);
  431. const TString text = "test output";
  432. auto afterForkCallback = [&tmpFilePath, &text]() -> void {
  433. TFixedBufferFileOutput out(tmpFilePath);
  434. out << text;
  435. };
  436. TShellCommandOptions options;
  437. options.SetFuncAfterFork(afterForkCallback);
  438. const TString command = "ls";
  439. TShellCommand cmd(command, options);
  440. cmd.Run();
  441. UNIT_ASSERT(NFs::Exists(tmpFilePath));
  442. TUnbufferedFileInput fileOutput(tmpFilePath);
  443. TString firstLine = fileOutput.ReadLine();
  444. UNIT_ASSERT_VALUES_EQUAL(firstLine, text);
  445. }
  446. } // Y_UNIT_TEST_SUITE(TShellCommandTest)