Module: PWN::Cron
- Defined in:
- lib/pwn/cron.rb
Overview
PWN::Cron provides cron / scheduled task management for the pwn-ai agent. Jobs are defined in ~/.pwn/cron/jobs.yml and can be triggered by system cron, manual run, or from within pwn-ai agent loops.
Each job can contain a prompt (for pwn-ai), a ruby script snippet, or reference to external script. Delivery can be 'log' (default), 'email', etc. (email would require additional plugins).
Constant Summary collapse
- CRON_DIR =
if ENV['PWN_CRON_DIR'].to_s.strip.empty? File.join(Dir.home, '.pwn', 'cron') else ENV.fetch('PWN_CRON_DIR', nil) end
- JOBS_FILE =
File.join(CRON_DIR, 'jobs.yml')
- DEFAULT_JOBS =
- Supported Method Parameters
PWN::Cron.install_defaults
Idempotently seed the RL feedback-loop cron jobs so a fresh install closes the loop by default (S1 practice + P3 offline_judge + W2 train dry-run + M1 consolidate). Re-running is a no-op if jobs with these names already exist. These are seeded to jobs.yml only — pass install_crontab: true to PWN::Cron.create yourself if you also want a system crontab entry.
[ { name: 'curriculum_practice_nightly', schedule: '0 3 * * *', ruby: 'PWN::AI::Agent::Curriculum.practice(limit: 3) if defined?(PWN::AI::Agent::Curriculum)' }, { name: 'curriculum_train_weekly', schedule: '0 4 * * 0', ruby: 'PWN::AI::Agent::Curriculum.train_and_gate(dry_run: true) if defined?(PWN::AI::Agent::Curriculum)' }, { name: 'curriculum_offline_judge', schedule: '30 3 * * *', ruby: 'PWN::AI::Agent::Curriculum.offline_judge(since_hours: 24, limit: 40) if defined?(PWN::AI::Agent::Curriculum)', aliases: %w[offline_judge_nightly] }, { name: 'learning_consolidate_nightly', schedule: '0 5 * * *', ruby: 'PWN::AI::Agent::Learning.consolidate if defined?(PWN::AI::Agent::Learning)' } ].freeze
- DEFAULT_INTERVAL =
In-process scheduler. System crontab is optional (install_crontab) and install_defaults never writes it, so last_run only moves when something ticks jobs.yml. The worker is that something: poll once or loop, fire due enabled jobs via PWN::Cron.run, and stay alive behind a pidfile so
pwn setupcan start / restart it.Spawned workers honor PWN_CRON_DIR so tests can sandbox the daemon without touching the operator's real ~/.pwn/cron.
60- DEFAULT_LOCK_STALE =
7_200- PID_FILE =
Kept for callers / older snippets. Prefer the methods above.
File.join(CRON_DIR, 'worker.pid')
- WORKER_LOG =
File.join(CRON_DIR, 'worker.log')
- SYSTEMD_WORKER_UNIT =
OS-agnostic native persistence. jobs.yml remains the source of truth (5-field cron + relative "every 30m"). The in-process worker evaluates due? and fires jobs. A native backend only (a) restarts that worker after reboot / login and (b) optionally plants a calendar trigger per 5-field job so the schedule still fires if the worker is down.
linux -> systemd --user (Restart=always) else crontab @reboot osx -> launchd LaunchAgent (RunAtLoad + KeepAlive) windows -> schtasks ONLOGON *bsd / cygwin -> crontab @reboot when crontab(1) exists else -> in-process worker only (lives with the login)
'pwn-cron-worker.service'- LAUNCHD_WORKER_LABEL =
'com.0dayinc.pwn.cron.worker'- SCHTASKS_WORKER_NAME =
'PWN\CronWorker'
Class Method Summary collapse
- .apply_native?(opts = {}) ⇒ Boolean
-
.authors ⇒ Object
- Author(s)
0day Inc.
-
.create(opts = {}) ⇒ Object
- Supported Method Parameters
job = PWN::Cron.create( name: 'optional', schedule: 'required e.g. "0 * * * *" or "30m" or "every 2h"', prompt: 'optional - pwn-ai prompt to run', ruby: 'optional - ruby snippet to eval', script: 'optional - path to external script', delivery: 'log|stdout (default log)', enabled: true ).
-
.cron_dir ⇒ Object
- Supported Method Parameters
dir = PWN::Cron.cron_dir.
- .cron_enabled? ⇒ Boolean
-
.cron_to_calendar(opts = {}) ⇒ Object
Convert a 5-field cron expression into a portable calendar hash.
- .crontab_available? ⇒ Boolean
- .disable(opts = {}) ⇒ Object
-
.due?(opts = {}) ⇒ Boolean
- Supported Method Parameters
PWN::Cron.due?( schedule: 'required - 5-field cron or relative ("30m", "every 2h")', last_run: 'optional - Time / iso8601 / nil', now: 'optional - Time (default Time.now)' ).
-
.due_jobs(opts = {}) ⇒ Object
- Supported Method Parameters
PWN::Cron.due_jobs(now: Time).
-
.enable(opts = {}) ⇒ Object
- Supported Method Parameters
PWN::Cron.enable/disable(id:).
-
.ensure_worker(opts = {}) ⇒ Object
PWN::Setup hook — start (or restart) the background worker after any setup action so YAML-only default jobs actually fire.
-
.help ⇒ Object
Display Usage for this Module.
-
.install_crontab_entry(opts = {}) ⇒ Object
Install a crontab line that invokes this job via pwn (assumes /opt/pwn and the active rvm ruby@pwn gemset - user can edit crontab).
- .install_crontab_job(opts = {}) ⇒ Object
-
.install_crontab_worker(opts = {}) ⇒ Object
---- crontab -------------------------------------------------------.
- .install_defaults ⇒ Object
- .install_launchd_job(opts = {}) ⇒ Object
-
.install_launchd_worker(opts = {}) ⇒ Object
---- launchd -------------------------------------------------------.
- .install_native_job(opts = {}) ⇒ Object
-
.install_scheduler(opts = {}) ⇒ Object
- Supported Method Parameters
PWN::Cron.install_scheduler( enabled: true, backend: optional force, apply: optional (default true unless PWN_CRON_DIR is set), sync_jobs: true ).
- .install_schtasks_job(opts = {}) ⇒ Object
-
.install_schtasks_worker(opts = {}) ⇒ Object
---- schtasks (Windows) -------------------------------------------.
- .install_systemd_user_job(opts = {}) ⇒ Object
-
.install_systemd_user_worker(opts = {}) ⇒ Object
---- systemd --user ------------------------------------------------.
-
.install_worker_crontab(opts = {}) ⇒ Object
- Supported Method Parameters
PWN::Cron.install_worker_crontab Append an @reboot line so the worker comes back after a reboot.
-
.jobs_file ⇒ Object
- Supported Method Parameters
PWN::Cron.jobs_file / PWN::Cron.pid_file / PWN::Cron.worker_log Runtime paths follow cron_dir (and therefore a stubbed CRON_DIR).
- .launchd_calendar_interval(opts = {}) ⇒ Object
-
.list ⇒ Object
- Supported Method Parameters
jobs = PWN::Cron.list.
- .native_unit_dir(opts = {}) ⇒ Object
- .os_type ⇒ Object
- .persist_scheduler_config(opts = {}) ⇒ Object
- .pid_file ⇒ Object
-
.remove(opts = {}) ⇒ Object
rubocop:disable Naming/PredicateMethod.
- .remove_native_job(opts = {}) ⇒ Object
- .ruby_eval_argv(opts = {}) ⇒ Object
- .ruby_eval_shell(opts = {}) ⇒ Object
- .ruby_lib_dir ⇒ Object
-
.run(opts = {}) ⇒ Object
- Supported Method Parameters
PWN::Cron.run(id: 'required or name') Executes the job (for pwn-ai prompt it will use current active AI engine via PWN::AI::* but without full REPL hook unless in pwn-ai).
-
.run_due(opts = {}) ⇒ Object
- Supported Method Parameters
PWN::Cron.run_due(now: Time, jobs: optional Hash) Fires every currently-due enabled job via run().
-
.scheduler_backend(opts = {}) ⇒ Object
- Supported Method Parameters
PWN::Cron.scheduler_backend(os: optional, backend: optional force).
- .scheduler_config ⇒ Object
- .scheduler_file ⇒ Object
- .scheduler_status(opts = {}) ⇒ Object
- .schtasks_available? ⇒ Boolean
- .schtasks_spec(opts = {}) ⇒ Object
-
.start_worker(opts = {}) ⇒ Object
- Supported Method Parameters
PWN::Cron.start_worker( interval: 60, foreground: false, restart: true ) Idempotent.
-
.stop_worker ⇒ Object
- Supported Method Parameters
PWN::Cron.stop_worker.
- .sync_native_jobs(opts = {}) ⇒ Object
- .sync_scheduler(opts = {}) ⇒ Object
- .systemd_on_calendar(opts = {}) ⇒ Object
- .systemd_user_available? ⇒ Boolean
-
.tick(opts = {}) ⇒ Object
Single tick used by the loop AND by tests (no sleep).
- .uninstall_crontab(opts = {}) ⇒ Object
- .uninstall_crontab_job(opts = {}) ⇒ Object
- .uninstall_launchd(opts = {}) ⇒ Object
- .uninstall_launchd_job(opts = {}) ⇒ Object
- .uninstall_scheduler(opts = {}) ⇒ Object
- .uninstall_schtasks(opts = {}) ⇒ Object
- .uninstall_schtasks_job(opts = {}) ⇒ Object
- .uninstall_systemd_user(opts = {}) ⇒ Object
- .uninstall_systemd_user_job(opts = {}) ⇒ Object
- .which_bin(opts = {}) ⇒ Object
- .windows? ⇒ Boolean
- .worker_log ⇒ Object
-
.worker_loop(opts = {}) ⇒ Object
rubocop:disable Naming/PredicateMethod.
-
.worker_status ⇒ Object
- Supported Method Parameters
PWN::Cron.worker_status.
Class Method Details
.apply_native?(opts = {}) ⇒ Boolean
769 770 771 772 773 774 775 |
# File 'lib/pwn/cron.rb', line 769 public_class_method def self.apply_native?(opts = {}) return opts[:apply] if opts.key?(:apply) return false unless ENV['PWN_CRON_DIR'].to_s.empty? default = File.join(Dir.home, '.pwn', 'cron') File.(cron_dir.to_s) == File.(default) end |
.authors ⇒ Object
- Author(s)
0day Inc. support@0dayinc.com
1502 1503 1504 |
# File 'lib/pwn/cron.rb', line 1502 public_class_method def self. "AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n" end |
.create(opts = {}) ⇒ Object
- Supported Method Parameters
job = PWN::Cron.create( name: 'optional', schedule: 'required e.g. "0 * * * *" or "30m" or "every 2h"', prompt: 'optional - pwn-ai prompt to run', ruby: 'optional - ruby snippet to eval', script: 'optional - path to external script', delivery: 'log|stdout (default log)', enabled: true )
50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
# File 'lib/pwn/cron.rb', line 50 public_class_method def self.create(opts = {}) jobs = load_jobs id = SecureRandom.hex(6) name = opts[:name] || "job-#{id}" job = { id: id, name: name, schedule: opts[:schedule] || '0 * * * *', prompt: opts[:prompt], ruby: opts[:ruby], script: opts[:script], delivery: opts[:delivery] || 'log', enabled: opts.fetch(:enabled, true), created_at: Time.now.utc.iso8601, last_run: nil, last_status: nil } jobs[id] = job save_jobs(jobs: jobs) # Optionally install a crontab entry (user must have permission) install_crontab_entry(job: job) if opts[:install_crontab] sync_one_job(job: job) if cron_enabled? && !opts[:install_crontab] job end |
.cron_dir ⇒ Object
- Supported Method Parameters
dir = PWN::Cron.cron_dir
29 30 31 32 |
# File 'lib/pwn/cron.rb', line 29 public_class_method def self.cron_dir FileUtils.mkdir_p(CRON_DIR) CRON_DIR end |
.cron_enabled? ⇒ Boolean
754 755 756 |
# File 'lib/pwn/cron.rb', line 754 public_class_method def self.cron_enabled? scheduler_config[:enabled] != false end |
.cron_to_calendar(opts = {}) ⇒ Object
Convert a 5-field cron expression into a portable calendar hash. Returns nil for relative schedules or expressions we cannot map.
876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 |
# File 'lib/pwn/cron.rb', line 876 public_class_method def self.cron_to_calendar(opts = {}) schedule = opts[:schedule].to_s.strip return nil if relative_seconds(schedule: schedule) fields = schedule.split return nil unless fields.length == 5 min, hour, mday, mon, wday = fields return nil unless min.match?(/\A\d+\z/) && hour.match?(/\A\d+\z/) return nil unless mon == '*' minute = min.to_i hr = hour.to_i return nil unless minute.between?(0, 59) && hr.between?(0, 23) if mday == '*' && wday == '*' { kind: :daily, minute: minute, hour: hr } elsif mday == '*' && wday.match?(/\A[0-7]\z/) wd = wday.to_i wd = 0 if wd == 7 { kind: :weekly, minute: minute, hour: hr, wday: wd } elsif wday == '*' && mday.match?(/\A\d+\z/) { kind: :monthly, minute: minute, hour: hr, mday: mday.to_i } end end |
.crontab_available? ⇒ Boolean
791 792 793 |
# File 'lib/pwn/cron.rb', line 791 public_class_method def self.crontab_available? !which_bin(name: 'crontab').empty? end |
.disable(opts = {}) ⇒ Object
159 160 161 |
# File 'lib/pwn/cron.rb', line 159 public_class_method def self.disable(opts = {}) toggle(id: opts[:id], enabled: false) end |
.due?(opts = {}) ⇒ Boolean
- Supported Method Parameters
PWN::Cron.due?( schedule: 'required - 5-field cron or relative ("30m", "every 2h")', last_run: 'optional - Time / iso8601 / nil', now: 'optional - Time (default Time.now)' )
325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 |
# File 'lib/pwn/cron.rb', line 325 public_class_method def self.due?(opts = {}) schedule = opts[:schedule].to_s.strip return false if schedule.empty? now = opts[:now] || Time.now last = parse_time(value: opts[:last_run]) if (secs = relative_seconds(schedule: schedule)) return true if last.nil? return (now - last) >= secs end fields = schedule.split return false unless fields.length == 5 prev = last || (now - (24 * 60 * 60)) # Walk minute-aligned slots from the minute AFTER last_run # (or the last 24h if never run) up to `now`. t = Time.at(((prev.to_i / 60) + 1) * 60) t = Time.local(t.year, t.month, t.day, t.hour, t.min, 0) limit = Time.local(now.year, now.month, now.day, now.hour, now.min, 0) safety = 0 while t <= limit && safety < 10_080 return true if cron_match?(fields: fields, time: t) t += 60 safety += 1 end false rescue StandardError false end |
.due_jobs(opts = {}) ⇒ Object
- Supported Method Parameters
PWN::Cron.due_jobs(now: Time)
361 362 363 364 365 366 367 368 369 370 |
# File 'lib/pwn/cron.rb', line 361 public_class_method def self.due_jobs(opts = {}) now = opts[:now] || Time.now load_jobs.each_with_object({}) do |(id, job), acc| next unless job.is_a?(Hash) next unless job[:enabled] next unless due?(schedule: job[:schedule], last_run: job[:last_run], now: now) acc[id] = job end end |
.enable(opts = {}) ⇒ Object
- Supported Method Parameters
PWN::Cron.enable/disable(id:)
155 156 157 |
# File 'lib/pwn/cron.rb', line 155 public_class_method def self.enable(opts = {}) toggle(id: opts[:id], enabled: true) end |
.ensure_worker(opts = {}) ⇒ Object
PWN::Setup hook — start (or restart) the background worker after any setup action so YAML-only default jobs actually fire.
526 527 528 529 530 531 532 |
# File 'lib/pwn/cron.rb', line 526 public_class_method def self.ensure_worker(opts = {}) start_worker( interval: opts[:interval] || DEFAULT_INTERVAL, restart: opts.fetch(:restart, false), foreground: false ) end |
.help ⇒ Object
Display Usage for this Module
1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 |
# File 'lib/pwn/cron.rb', line 1507 public_class_method def self.help puts <<~USAGE USAGE: PWN::Cron.create(schedule: '0 * * * *', prompt: 'Run daily recon on target.com using NmapIt and report', name: 'daily-recon') PWN::Cron.list res = PWN::Cron.run(id: 'abc123') PWN::Cron.enable(id: 'abc123') PWN::Cron.disable(id: 'abc123') PWN::Cron.remove(id: 'abc123') # OS-native persistence (systemd --user / launchd / schtasks / crontab) PWN::Cron.install_scheduler PWN::Cron.uninstall_scheduler PWN::Cron.sync_scheduler PWN::Cron.scheduler_backend PWN::Cron.install_defaults # seed S1/P3/W2/M1 curriculum jobs (idempotent) PWN::Cron.ensure_worker # start/restart the background scheduler PWN::Cron.worker_status PWN::Cron.tick # fire currently-due jobs once #{self}.authors USAGE end |
.install_crontab_entry(opts = {}) ⇒ Object
Install a crontab line that invokes this job via pwn (assumes /opt/pwn and the active rvm ruby@pwn gemset - user can edit crontab)
165 166 167 |
# File 'lib/pwn/cron.rb', line 165 public_class_method def self.install_crontab_entry(opts = {}) install_native_job(opts.merge(backend: :crontab)) end |
.install_crontab_job(opts = {}) ⇒ Object
1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 |
# File 'lib/pwn/cron.rb', line 1078 public_class_method def self.install_crontab_job(opts = {}) job = opts[:job] apply = apply_native?(opts) snippet = "require \"pwn\"; PWN::Cron.run(id: #{job[:id].to_s.inspect})" line = "#{job[:schedule]} #{ruby_eval_shell(snippet: snippet, log: File.join(cron_dir, 'cron.log'))}" block = "# pwn-cron #{job[:name]} (#{job[:id]})\n#{line}\n" write_native_preview(name: "job-#{job[:id]}.crontab", body: block) return { backend: :crontab, apply: false, line: line, id: job[:id] } unless apply && crontab_available? existing = crontab_read return { backend: :crontab, already: true, id: job[:id], line: line } if existing.include?(job[:id].to_s) crontab_write(body: "#{existing.rstrip}\n#{block}") { backend: :crontab, apply: true, id: job[:id], line: line } end |
.install_crontab_worker(opts = {}) ⇒ Object
---- crontab -------------------------------------------------------
1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 |
# File 'lib/pwn/cron.rb', line 1063 public_class_method def self.install_crontab_worker(opts = {}) apply = apply_native?(opts) marker = 'PWN::Cron.ensure_worker' line = "@reboot #{ruby_eval_shell(snippet: 'require "pwn"; PWN::Cron.ensure_worker(restart: false)', log: worker_log)}" preview = "# pwn-cron worker\n#{line}\n" write_native_preview(name: 'worker.crontab', body: preview) return { backend: :crontab, apply: false, line: line } unless apply && crontab_available? existing = crontab_read return { backend: :crontab, already: true, line: line } if existing.include?(marker) crontab_write(body: "#{existing.rstrip}\n#{preview}") { backend: :crontab, apply: true, line: line } end |
.install_defaults ⇒ Object
250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 |
# File 'lib/pwn/cron.rb', line 250 public_class_method def self.install_defaults jobs = load_jobs names = jobs.values.map { |j| j[:name].to_s } seeded = [] DEFAULT_JOBS.each do |spec| aliases = Array(spec[:aliases]).map(&:to_s) known = [spec[:name].to_s, *aliases] next if names.intersect?(known) seeded << create( name: spec[:name], schedule: spec[:schedule], ruby: spec[:ruby], delivery: 'log', enabled: true ) names << spec[:name].to_s end # P13 - disable the legacy alias when both occupy the 30 3 * * * slot. if names.include?('curriculum_offline_judge') && names.include?('offline_judge_nightly') begin dup = jobs.values.find { |j| j[:name].to_s == 'offline_judge_nightly' } disable(id: dup[:id]) if dup && dup[:enabled] rescue StandardError nil end end seeded rescue StandardError => e warn("[PWN::Cron] install_defaults failed: #{e.class}: #{e.}") [] end |
.install_launchd_job(opts = {}) ⇒ Object
1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 |
# File 'lib/pwn/cron.rb', line 1237 public_class_method def self.install_launchd_job(opts = {}) job = opts[:job] apply = apply_native?(opts) ints = launchd_calendar_interval(schedule: job[:schedule]) return { backend: :launchd, id: job[:id], skipped: true, reason: :no_calendar } unless ints label = launchd_job_label(job: job) argv = ruby_eval_argv(snippet: "require \"pwn\"; PWN::Cron.run(id: #{job[:id].to_s.inspect})") plist = launchd_plist(label: label, argv: argv, keepalive: false, run_at_load: false, calendar: ints) path = write_unit_file(backend: :launchd, name: "#{label}.plist", body: plist, apply: apply) launchctl_load(path: path, label: label) if apply { backend: :launchd, id: job[:id], label: label, path: path, calendar: ints, apply: apply } end |
.install_launchd_worker(opts = {}) ⇒ Object
---- launchd -------------------------------------------------------
1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 |
# File 'lib/pwn/cron.rb', line 1223 public_class_method def self.install_launchd_worker(opts = {}) apply = apply_native?(opts) label = LAUNCHD_WORKER_LABEL plist = launchd_plist( label: label, argv: ruby_eval_argv(snippet: 'require "pwn"; PWN::Cron.start_worker(restart: false, foreground: true)'), keepalive: true, run_at_load: true ) path = write_unit_file(backend: :launchd, name: "#{label}.plist", body: plist, apply: apply) launchctl_load(path: path, label: label) if apply { backend: :launchd, label: label, path: path, apply: apply } end |
.install_native_job(opts = {}) ⇒ Object
1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 |
# File 'lib/pwn/cron.rb', line 1018 public_class_method def self.install_native_job(opts = {}) job = opts[:job] return nil unless job.is_a?(Hash) && job[:id] backend = (opts[:backend] || scheduler_backend).to_sym apply = apply_native?(opts) case backend when :crontab then install_crontab_job(job: job, apply: apply) when :systemd_user then install_systemd_user_job(job: job, apply: apply) when :launchd then install_launchd_job(job: job, apply: apply) when :schtasks then install_schtasks_job(job: job, apply: apply) else { backend: backend, skipped: true, reason: :worker_only } end end |
.install_scheduler(opts = {}) ⇒ Object
- Supported Method Parameters
PWN::Cron.install_scheduler( enabled: true, backend: optional force, apply: optional (default true unless PWN_CRON_DIR is set), sync_jobs: true )
946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 |
# File 'lib/pwn/cron.rb', line 946 public_class_method def self.install_scheduler(opts = {}) enabled = opts.fetch(:enabled, true) return uninstall_scheduler(opts) unless enabled backend = scheduler_backend(opts) apply = apply_native?(opts) %i[systemd_user launchd schtasks crontab].each do |other| next if other == backend uninstall_scheduler(backend: other, apply: apply, persist: false, stop: false) end persist_scheduler_config(enabled: true, backend: backend) result = case backend when :systemd_user then install_systemd_user_worker(apply: apply) when :launchd then install_launchd_worker(apply: apply) when :schtasks then install_schtasks_worker(apply: apply) when :crontab then install_crontab_worker(apply: apply) else { backend: :worker, persisted: false, note: 'in-process worker only' } end jobs_sync = if opts.fetch(:sync_jobs, true) sync_native_jobs(backend: backend, apply: apply) else [] end { ok: true, backend: backend, apply: apply, worker: result, jobs: jobs_sync } rescue StandardError => e warn("[PWN::Cron] install_scheduler failed: #{e.class}: #{e.}") { ok: false, error: "#{e.class}: #{e.}" } end |
.install_schtasks_job(opts = {}) ⇒ Object
1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 |
# File 'lib/pwn/cron.rb', line 1288 public_class_method def self.install_schtasks_job(opts = {}) job = opts[:job] apply = apply_native?(opts) spec = schtasks_spec(schedule: job[:schedule]) return { backend: :schtasks, id: job[:id], skipped: true, reason: :no_calendar } unless spec name = schtasks_job_name(job: job) tr = ruby_eval_shell(snippet: "require \"pwn\"; PWN::Cron.run(id: #{job[:id].to_s.inspect})") argv = ['schtasks', '/Create', '/F', '/TN', name, '/SC', spec[:sc], '/ST', spec[:st], '/RL', 'LIMITED', '/TR', tr] argv += ['/D', spec[:d]] if spec[:d] write_native_preview(name: "job-#{job[:id]}.schtasks.txt", body: argv.inspect) run_argv(argv: argv) if apply && schtasks_available? { backend: :schtasks, id: job[:id], name: name, spec: spec, apply: apply } end |
.install_schtasks_worker(opts = {}) ⇒ Object
---- schtasks (Windows) -------------------------------------------
1279 1280 1281 1282 1283 1284 1285 1286 |
# File 'lib/pwn/cron.rb', line 1279 public_class_method def self.install_schtasks_worker(opts = {}) apply = apply_native?(opts) tr = ruby_eval_shell(snippet: 'require "pwn"; PWN::Cron.ensure_worker(restart: false)') argv = ['schtasks', '/Create', '/F', '/TN', SCHTASKS_WORKER_NAME, '/SC', 'ONLOGON', '/RL', 'LIMITED', '/TR', tr] write_native_preview(name: 'worker.schtasks.txt', body: argv.inspect) run_argv(argv: argv) if apply && schtasks_available? { backend: :schtasks, name: SCHTASKS_WORKER_NAME, tr: tr, apply: apply } end |
.install_systemd_user_job(opts = {}) ⇒ Object
1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 |
# File 'lib/pwn/cron.rb', line 1148 public_class_method def self.install_systemd_user_job(opts = {}) job = opts[:job] apply = apply_native?(opts) cal = cron_to_calendar(schedule: job[:schedule]) oncal = systemd_on_calendar(calendar: cal) return { backend: :systemd_user, id: job[:id], skipped: true, reason: :no_calendar } unless oncal svc = systemd_job_service(job: job) tmr = systemd_job_timer(job: job) snippet = "require \"pwn\"; PWN::Cron.run(id: #{job[:id].to_s.inspect})" exec_start = ruby_eval_shell(snippet: snippet) svc_body = <<~UNIT [Unit] Description=PWN cron job #{job[:name]} (#{job[:id]}) [Service] Type=oneshot ExecStart=#{exec_start} Environment=PWN_CRON_DIR=#{cron_dir} UNIT tmr_body = <<~UNIT [Unit] Description=PWN cron timer #{job[:name]} (#{job[:schedule]}) [Timer] OnCalendar=#{oncal} Persistent=true Unit=#{svc} [Install] WantedBy=timers.target UNIT svc_path = write_unit_file(backend: :systemd_user, name: svc, body: svc_body, apply: apply) tmr_path = write_unit_file(backend: :systemd_user, name: tmr, body: tmr_body, apply: apply) if apply systemd_user(args: ['daemon-reload']) systemd_user(args: ['enable', '--now', tmr]) end { backend: :systemd_user, id: job[:id], service: svc, timer: tmr, on_calendar: oncal, paths: [svc_path, tmr_path], apply: apply } end |
.install_systemd_user_worker(opts = {}) ⇒ Object
---- systemd --user ------------------------------------------------
1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 |
# File 'lib/pwn/cron.rb', line 1118 public_class_method def self.install_systemd_user_worker(opts = {}) apply = apply_native?(opts) exec_start = ruby_eval_shell( snippet: 'require "pwn"; PWN::Cron.start_worker(restart: false, foreground: true)' ) body = <<~UNIT [Unit] Description=PWN cron worker (evaluates ~/.pwn/cron/jobs.yml schedules) After=default.target [Service] Type=simple ExecStart=#{exec_start} Restart=always RestartSec=5 Environment=PWN_CRON_DIR=#{cron_dir} WorkingDirectory=#{ruby_lib_dir} [Install] WantedBy=default.target UNIT path = write_unit_file(backend: :systemd_user, name: SYSTEMD_WORKER_UNIT, body: body, apply: apply) if apply systemd_user(args: ['daemon-reload']) systemd_user(args: ['enable', '--now', SYSTEMD_WORKER_UNIT]) enable_linger end { backend: :systemd_user, unit: SYSTEMD_WORKER_UNIT, path: path, apply: apply } end |
.install_worker_crontab(opts = {}) ⇒ Object
- Supported Method Parameters
PWN::Cron.install_worker_crontab
Append an @reboot line so the worker comes back after a reboot. Idempotent. Does not remove existing per-job crontab lines.
538 539 540 541 542 543 |
# File 'lib/pwn/cron.rb', line 538 public_class_method def self.install_worker_crontab(opts = {}) install_crontab_worker(opts) rescue StandardError => e warn("[PWN::Cron] install_worker_crontab failed: #{e.class}: #{e.}") nil end |
.jobs_file ⇒ Object
- Supported Method Parameters
PWN::Cron.jobs_file / PWN::Cron.pid_file / PWN::Cron.worker_log
Runtime paths follow cron_dir (and therefore a stubbed CRON_DIR).
303 304 305 |
# File 'lib/pwn/cron.rb', line 303 public_class_method def self.jobs_file File.join(cron_dir, 'jobs.yml') end |
.launchd_calendar_interval(opts = {}) ⇒ Object
916 917 918 919 920 921 922 923 924 |
# File 'lib/pwn/cron.rb', line 916 public_class_method def self.launchd_calendar_interval(opts = {}) cal = opts[:calendar] || cron_to_calendar(schedule: opts[:schedule]) return nil unless cal ints = { 'Hour' => cal[:hour], 'Minute' => cal[:minute] } ints['Weekday'] = cal[:wday] if cal[:kind] == :weekly ints['Day'] = cal[:mday] if cal[:kind] == :monthly ints end |
.list ⇒ Object
- Supported Method Parameters
jobs = PWN::Cron.list
36 37 38 |
# File 'lib/pwn/cron.rb', line 36 public_class_method def self.list load_jobs end |
.native_unit_dir(opts = {}) ⇒ Object
838 839 840 841 842 843 844 845 846 847 848 849 850 851 |
# File 'lib/pwn/cron.rb', line 838 public_class_method def self.native_unit_dir(opts = {}) backend = (opts[:backend] || scheduler_backend).to_sym sandboxed = !ENV['PWN_CRON_DIR'].to_s.empty? || !apply_native?(opts) return File.join(cron_dir, 'native') if sandboxed case backend when :launchd File.join(Dir.home, 'Library', 'LaunchAgents') when :systemd_user File.join(Dir.home, '.config', 'systemd', 'user') else File.join(cron_dir, 'native') end end |
.os_type ⇒ Object
708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 |
# File 'lib/pwn/cron.rb', line 708 public_class_method def self.os_type t = begin PWN::Plugins::DetectOS.type rescue StandardError nil end return t if t host = RbConfig::CONFIG['host_os'].to_s.downcase return :windows if host.match?(/mswin|mingw|bccwin|wince|emc/) return :cygwin if host.include?('cygwin') return :osx if host.include?('darwin') return :linux if host.include?('linux') return :freebsd if host.include?('freebsd') return :netbsd if host.include?('netbsd') return :openbsd if host.include?('openbsd') :unknown end |
.persist_scheduler_config(opts = {}) ⇒ Object
758 759 760 761 762 763 764 765 766 767 |
# File 'lib/pwn/cron.rb', line 758 public_class_method def self.persist_scheduler_config(opts = {}) cfg = scheduler_config.merge( enabled: opts.fetch(:enabled, cron_enabled?), backend: (opts[:backend] || scheduler_config[:backend])&.to_sym, updated_at: Time.now.utc.iso8601 ) FileUtils.mkdir_p(cron_dir) File.write(scheduler_file, YAML.dump(cfg)) cfg end |
.pid_file ⇒ Object
307 308 309 |
# File 'lib/pwn/cron.rb', line 307 public_class_method def self.pid_file File.join(cron_dir, 'worker.pid') end |
.remove(opts = {}) ⇒ Object
rubocop:disable Naming/PredicateMethod
144 145 146 147 148 149 150 151 |
# File 'lib/pwn/cron.rb', line 144 public_class_method def self.remove(opts = {}) # rubocop:disable Naming/PredicateMethod id = opts[:id].to_s jobs = load_jobs gone = jobs.delete(id) save_jobs(jobs: jobs) remove_native_job(job: gone) if gone true end |
.remove_native_job(opts = {}) ⇒ Object
1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 |
# File 'lib/pwn/cron.rb', line 1034 public_class_method def self.remove_native_job(opts = {}) job = opts[:job] || { id: opts[:id], name: opts[:name] } backend = (opts[:backend] || scheduler_backend).to_sym apply = apply_native?(opts) case backend when :crontab then uninstall_crontab_job(job: job, apply: apply) when :systemd_user then uninstall_systemd_user_job(job: job, apply: apply) when :launchd then uninstall_launchd_job(job: job, apply: apply) when :schtasks then uninstall_schtasks_job(job: job, apply: apply) else { backend: backend, skipped: true } end end |
.ruby_eval_argv(opts = {}) ⇒ Object
857 858 859 860 |
# File 'lib/pwn/cron.rb', line 857 public_class_method def self.ruby_eval_argv(opts = {}) snippet = opts[:snippet].to_s [RbConfig.ruby, '-I', ruby_lib_dir, '-e', snippet] end |
.ruby_eval_shell(opts = {}) ⇒ Object
862 863 864 865 866 867 868 869 870 871 872 |
# File 'lib/pwn/cron.rb', line 862 public_class_method def self.ruby_eval_shell(opts = {}) argv = ruby_eval_argv(opts) log = opts[:log] if windows? cmd = argv.map { |a| /[\s"]/.match?(a) ? "\"#{a.gsub('"', '""')}\"" : a }.join(' ') log ? "#{cmd} >> #{log} 2>&1" : cmd else cmd = Shellwords.join(argv) log ? "#{cmd} >> #{Shellwords.escape(log.to_s)} 2>&1" : cmd end end |
.ruby_lib_dir ⇒ Object
853 854 855 |
# File 'lib/pwn/cron.rb', line 853 public_class_method def self.ruby_lib_dir File.('..', __dir__) end |
.run(opts = {}) ⇒ Object
- Supported Method Parameters
PWN::Cron.run(id: 'required or name') Executes the job (for pwn-ai prompt it will use current active AI engine via PWN::AI::* but without full REPL hook unless in pwn-ai).
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 |
# File 'lib/pwn/cron.rb', line 81 public_class_method def self.run(opts = {}) id = opts[:id].to_s jobs = load_jobs job = jobs[id] || jobs.values.find { |j| j[:name] == id || j[:id] == id } raise "Job #{id} not found" unless job start = Time.now result = nil status = 'success' begin if job[:prompt] engine = begin PWN::Env[:ai][:active].to_s.downcase.to_sym rescue StandardError :grok end case engine when :grok result = PWN::AI::Grok.chat(request: job[:prompt], spinner: false) when :ollama result = PWN::AI::Ollama.chat(request: job[:prompt], spinner: false) when :openai result = PWN::AI::OpenAI.chat(request: job[:prompt], spinner: false) when :anthropic result = PWN::AI::Anthropic.chat(request: job[:prompt], spinner: false) when :gemini result = PWN::AI::Gemini.chat(request: job[:prompt], spinner: false) end result = begin result[:choices].last[:content] rescue StandardError result.to_s end elsif job[:ruby] result = eval(job[:ruby], TOPLEVEL_BINDING) # rubocop:disable Security/Eval elsif job[:script] && File.exist?(job[:script]) result = `#{job[:script]} 2>&1` else result = 'No prompt/ruby/script defined' end if job[:delivery] == 'log' log_path = File.join(cron_dir, "#{job[:id]}.log") File.open(log_path, 'a') do |f| f.puts("[#{Time.now}] RUN #{job[:name]} (#{job[:id]})\n#{result}\n---") end end rescue StandardError => e status = 'error' result = "ERROR: #{e.class} - #{e.}\n#{e.backtrace.first(5).join("\n")}" end job[:last_run] = Time.now.utc.iso8601 job[:last_status] = status jobs[job[:id]] = job save_jobs(jobs: jobs) { job: job, result: result, duration: Time.now - start, status: status } end |
.run_due(opts = {}) ⇒ Object
- Supported Method Parameters
PWN::Cron.run_due(now: Time, jobs: optional Hash)
Fires every currently-due enabled job via run(). Returns the list of status:, duration: hashes (empty when nothing is due).
376 377 378 379 380 381 382 383 384 385 |
# File 'lib/pwn/cron.rb', line 376 public_class_method def self.run_due(opts = {}) now = opts[:now] || Time.now jobs = opts[:jobs] || due_jobs(now: now) jobs.map do |id, _job| res = run(id: id) { id: id, status: res[:status], duration: res[:duration] } rescue StandardError => e { id: id, status: 'error', error: "#{e.class}: #{e.}" } end end |
.scheduler_backend(opts = {}) ⇒ Object
- Supported Method Parameters
PWN::Cron.scheduler_backend(os: optional, backend: optional force)
811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 |
# File 'lib/pwn/cron.rb', line 811 public_class_method def self.scheduler_backend(opts = {}) return opts[:backend].to_sym if opts[:backend] unless opts[:os] persisted = scheduler_config[:backend] return persisted if persisted end os = (opts[:os] || os_type).to_sym case os when :windows schtasks_available? ? :schtasks : :worker when :osx :launchd when :linux if systemd_user_available? :systemd_user elsif crontab_available? :crontab else :worker end else crontab_available? ? :crontab : :worker end end |
.scheduler_config ⇒ Object
736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 |
# File 'lib/pwn/cron.rb', line 736 public_class_method def self.scheduler_config path = scheduler_file return { enabled: true, backend: nil } unless File.exist?(path) raw = YAML.safe_load_file( path, permitted_classes: [Symbol, Time], symbolize_names: true ) || {} { enabled: raw.key?(:enabled) ? raw[:enabled] == true : true, backend: raw[:backend]&.to_sym, updated_at: raw[:updated_at] } rescue StandardError { enabled: true, backend: nil } end |
.scheduler_file ⇒ Object
732 733 734 |
# File 'lib/pwn/cron.rb', line 732 public_class_method def self.scheduler_file File.join(cron_dir, 'scheduler.yml') end |
.scheduler_status(opts = {}) ⇒ Object
1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 |
# File 'lib/pwn/cron.rb', line 1005 public_class_method def self.scheduler_status(opts = {}) backend = scheduler_backend(opts) { os: os_type, backend: backend, enabled: cron_enabled?, apply: apply_native?(opts), config: scheduler_config, worker: worker_status, unit_dir: native_unit_dir(backend: backend, apply: apply_native?(opts)) } end |
.schtasks_available? ⇒ Boolean
805 806 807 |
# File 'lib/pwn/cron.rb', line 805 public_class_method def self.schtasks_available? !which_bin(name: 'schtasks').empty? end |
.schtasks_spec(opts = {}) ⇒ Object
926 927 928 929 930 931 932 933 934 935 936 937 938 |
# File 'lib/pwn/cron.rb', line 926 public_class_method def self.schtasks_spec(opts = {}) cal = opts[:calendar] || cron_to_calendar(schedule: opts[:schedule]) return nil unless cal st = format('%<h>02d:%<m>02d', h: cal[:hour], m: cal[:minute]) case cal[:kind] when :daily then { sc: 'DAILY', st: st } when :weekly days = %w[SUN MON TUE WED THU FRI SAT] { sc: 'WEEKLY', st: st, d: days[cal[:wday]] } when :monthly then { sc: 'MONTHLY', st: st, d: cal[:mday].to_s } end end |
.start_worker(opts = {}) ⇒ Object
- Supported Method Parameters
PWN::Cron.start_worker( interval: 60, foreground: false, restart: true )
Idempotent. With restart:true (default) a live worker is replaced
so pwn setup always leaves a current worker running.
408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 |
# File 'lib/pwn/cron.rb', line 408 public_class_method def self.start_worker(opts = {}) interval = (opts[:interval] || DEFAULT_INTERVAL).to_i interval = DEFAULT_INTERVAL if interval <= 0 foreground = opts[:foreground] ? true : false restart = opts.fetch(:restart, true) st = worker_status return { started: false, already_running: true, pid: st[:pid], interval: interval } if st[:running] && !restart && !foreground stop_worker if st[:running] && (restart || foreground) if foreground write_pid(pid: Process.pid) begin worker_loop(interval: interval) ensure clear_pid(pid: Process.pid) end return { started: true, pid: Process.pid, foreground: true, interval: interval } end FileUtils.mkdir_p(cron_dir) spawn_env = ENV.to_h.merge('PWN_CRON_DIR' => cron_dir.to_s) pid = Process.spawn( spawn_env, RbConfig.ruby, '-I', File.('..', __dir__), '-e', worker_spawn_snippet(interval: interval), %i[out err] => [worker_log, 'a'], in: File::NULL, pgroup: !windows? ) Process.detach(pid) # The child writes the pidfile; wait briefly. waited = 0 live = nil while waited < 50 live = read_pid break if live && process_alive?(pid: live) sleep 0.1 waited += 1 end { started: !(live && process_alive?(pid: live)).nil?, pid: live, spawn_pid: pid, interval: interval, log: worker_log, pid_file: pid_file } end |
.stop_worker ⇒ Object
- Supported Method Parameters
PWN::Cron.stop_worker
464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 |
# File 'lib/pwn/cron.rb', line 464 public_class_method def self.stop_worker pid = read_pid killed = false if pid && process_alive?(pid: pid) begin Process.kill('TERM', pid) 20.times do break unless process_alive?(pid: pid) sleep 0.05 end Process.kill('KILL', pid) if process_alive?(pid: pid) killed = true rescue Errno::ESRCH, Errno::EPERM nil rescue StandardError windows_taskkill(pid: pid) if windows? end end FileUtils.rm_f(pid_file) { stopped: true, pid: pid, killed: killed } end |
.sync_native_jobs(opts = {}) ⇒ Object
1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 |
# File 'lib/pwn/cron.rb', line 1048 public_class_method def self.sync_native_jobs(opts = {}) backend = (opts[:backend] || scheduler_backend).to_sym apply = apply_native?(opts) load_jobs.map do |_id, job| next unless job.is_a?(Hash) if job[:enabled] install_native_job(job: job, backend: backend, apply: apply) else remove_native_job(job: job, backend: backend, apply: apply) end end.compact end |
.sync_scheduler(opts = {}) ⇒ Object
999 1000 1001 1002 1003 |
# File 'lib/pwn/cron.rb', line 999 public_class_method def self.sync_scheduler(opts = {}) return uninstall_scheduler(opts) unless opts.fetch(:enabled, cron_enabled?) install_scheduler(opts.merge(sync_jobs: true)) end |
.systemd_on_calendar(opts = {}) ⇒ Object
902 903 904 905 906 907 908 909 910 911 912 913 914 |
# File 'lib/pwn/cron.rb', line 902 public_class_method def self.systemd_on_calendar(opts = {}) cal = opts[:calendar] || cron_to_calendar(schedule: opts[:schedule]) return nil unless cal hhmmss = format('%<h>02d:%<m>02d:00', h: cal[:hour], m: cal[:minute]) case cal[:kind] when :daily then "*-*-* #{hhmmss}" when :weekly dow = %w[Sun Mon Tue Wed Thu Fri Sat][cal[:wday]] "#{dow} *-*-* #{hhmmss}" when :monthly then "*-*-#{format('%<d>02d', d: cal[:mday])} #{hhmmss}" end end |
.systemd_user_available? ⇒ Boolean
795 796 797 798 799 800 801 802 803 |
# File 'lib/pwn/cron.rb', line 795 public_class_method def self.systemd_user_available? return false if which_bin(name: 'systemctl').empty? return false unless apply_native? system('systemctl', '--user', 'show-environment', out: File::NULL, err: File::NULL) rescue StandardError false end |
.tick(opts = {}) ⇒ Object
Single tick used by the loop AND by tests (no sleep).
488 489 490 |
# File 'lib/pwn/cron.rb', line 488 public_class_method def self.tick(opts = {}) run_due(now: opts[:now] || Time.now) end |
.uninstall_crontab(opts = {}) ⇒ Object
1094 1095 1096 1097 1098 1099 1100 1101 1102 |
# File 'lib/pwn/cron.rb', line 1094 public_class_method def self.uninstall_crontab(opts = {}) apply = apply_native?(opts) return [:preview] unless apply && crontab_available? existing = crontab_read cleaned = strip_pwn_crontab(existing: existing) crontab_write(body: cleaned) if cleaned != existing [:crontab] end |
.uninstall_crontab_job(opts = {}) ⇒ Object
1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 |
# File 'lib/pwn/cron.rb', line 1104 public_class_method def self.uninstall_crontab_job(opts = {}) job = opts[:job] apply = apply_native?(opts) FileUtils.rm_f(File.join(cron_dir, 'native', "job-#{job[:id]}.crontab")) return { backend: :crontab, apply: false, id: job[:id] } unless apply && crontab_available? existing = crontab_read cleaned = strip_pwn_crontab(existing: existing, id: job[:id].to_s) crontab_write(body: cleaned) if cleaned != existing { backend: :crontab, apply: true, id: job[:id], removed: true } end |
.uninstall_launchd(opts = {}) ⇒ Object
1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 |
# File 'lib/pwn/cron.rb', line 1251 public_class_method def self.uninstall_launchd(opts = {}) apply = apply_native?(opts) labels = [LAUNCHD_WORKER_LABEL] load_jobs.each_value do |job| next unless job.is_a?(Hash) labels << launchd_job_label(job: job) end labels.each do |label| path = unit_file_path(backend: :launchd, name: "#{label}.plist", apply: apply) launchctl_unload(path: path, label: label) if apply rm_unit_file(backend: :launchd, name: "#{label}.plist", apply: apply) end labels end |
.uninstall_launchd_job(opts = {}) ⇒ Object
1267 1268 1269 1270 1271 1272 1273 1274 1275 |
# File 'lib/pwn/cron.rb', line 1267 public_class_method def self.uninstall_launchd_job(opts = {}) job = opts[:job] apply = apply_native?(opts) label = launchd_job_label(job: job) path = unit_file_path(backend: :launchd, name: "#{label}.plist", apply: apply) launchctl_unload(path: path, label: label) if apply rm_unit_file(backend: :launchd, name: "#{label}.plist", apply: apply) { backend: :launchd, id: job[:id], removed: label } end |
.uninstall_scheduler(opts = {}) ⇒ Object
979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 |
# File 'lib/pwn/cron.rb', line 979 public_class_method def self.uninstall_scheduler(opts = {}) backend = (opts[:backend] || scheduler_config[:backend] || scheduler_backend).to_sym apply = apply_native?(opts) persist_scheduler_config(enabled: false, backend: backend) unless opts[:persist] == false stop_worker if opts.fetch(:stop, true) removed = case backend when :systemd_user then uninstall_systemd_user(apply: apply) when :launchd then uninstall_launchd(apply: apply) when :schtasks then uninstall_schtasks(apply: apply) when :crontab then uninstall_crontab(apply: apply) else [] end { ok: true, backend: backend, apply: apply, removed: removed } rescue StandardError => e warn("[PWN::Cron] uninstall_scheduler failed: #{e.class}: #{e.}") { ok: false, error: "#{e.class}: #{e.}" } end |
.uninstall_schtasks(opts = {}) ⇒ Object
1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 |
# File 'lib/pwn/cron.rb', line 1303 public_class_method def self.uninstall_schtasks(opts = {}) apply = apply_native?(opts) names = [SCHTASKS_WORKER_NAME] load_jobs.each_value do |job| next unless job.is_a?(Hash) names << schtasks_job_name(job: job) end names.each { |n| run_argv(argv: ['schtasks', '/Delete', '/F', '/TN', n]) if apply && schtasks_available? } names end |
.uninstall_schtasks_job(opts = {}) ⇒ Object
1315 1316 1317 1318 1319 1320 1321 1322 |
# File 'lib/pwn/cron.rb', line 1315 public_class_method def self.uninstall_schtasks_job(opts = {}) job = opts[:job] apply = apply_native?(opts) name = schtasks_job_name(job: job) run_argv(argv: ['schtasks', '/Delete', '/F', '/TN', name]) if apply && schtasks_available? FileUtils.rm_f(File.join(cron_dir, 'native', "job-#{job[:id]}.schtasks.txt")) { backend: :schtasks, id: job[:id], removed: name } end |
.uninstall_systemd_user(opts = {}) ⇒ Object
1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 |
# File 'lib/pwn/cron.rb', line 1190 public_class_method def self.uninstall_systemd_user(opts = {}) apply = apply_native?(opts) names = [SYSTEMD_WORKER_UNIT] load_jobs.each_value do |job| next unless job.is_a?(Hash) names << systemd_job_timer(job: job) names << systemd_job_service(job: job) end if apply names.each { |u| systemd_user(args: ['disable', '--now', u]) } systemd_user(args: ['daemon-reload']) end names.each { |n| rm_unit_file(backend: :systemd_user, name: n, apply: apply) } names end |
.uninstall_systemd_user_job(opts = {}) ⇒ Object
1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 |
# File 'lib/pwn/cron.rb', line 1207 public_class_method def self.uninstall_systemd_user_job(opts = {}) job = opts[:job] apply = apply_native?(opts) svc = systemd_job_service(job: job) tmr = systemd_job_timer(job: job) if apply systemd_user(args: ['disable', '--now', tmr]) systemd_user(args: ['disable', '--now', svc]) systemd_user(args: ['daemon-reload']) end rm_unit_file(backend: :systemd_user, name: svc, apply: apply) rm_unit_file(backend: :systemd_user, name: tmr, apply: apply) { backend: :systemd_user, id: job[:id], removed: [svc, tmr] } end |
.which_bin(opts = {}) ⇒ Object
777 778 779 780 781 782 783 784 785 786 787 788 789 |
# File 'lib/pwn/cron.rb', line 777 public_class_method def self.which_bin(opts = {}) name = opts[:name].to_s return '' if name.empty? exts = windows? ? %w[.exe .bat .cmd] + [''] : [''] ENV.fetch('PATH', '').split(File::PATH_SEPARATOR).each do |dir| exts.each do |ext| cand = File.join(dir, "#{name}#{ext}") return cand if File.file?(cand) && File.executable?(cand) end end '' end |
.windows? ⇒ Boolean
728 729 730 |
# File 'lib/pwn/cron.rb', line 728 public_class_method def self.windows? os_type == :windows end |
.worker_log ⇒ Object
311 312 313 |
# File 'lib/pwn/cron.rb', line 311 public_class_method def self.worker_log File.join(cron_dir, 'worker.log') end |
.worker_loop(opts = {}) ⇒ Object
rubocop:disable Naming/PredicateMethod
495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 |
# File 'lib/pwn/cron.rb', line 495 public_class_method def self.worker_loop(opts = {}) # rubocop:disable Naming/PredicateMethod interval = (opts[:interval] || DEFAULT_INTERVAL).to_i interval = DEFAULT_INTERVAL if interval <= 0 once = opts[:once] ? true : false trap_shutdown! unless once loop do begin tick rescue StandardError => e warn("[PWN::Cron] worker tick failed: #{e.class}: #{e.}") end break if once break if @worker_stop break if File.exist?(pid_file) && read_pid != Process.pid slept = 0 while slept < interval break if @worker_stop step = [1, interval - slept].min sleep(step) slept += step end break if @worker_stop end true end |
.worker_status ⇒ Object
- Supported Method Parameters
PWN::Cron.worker_status
389 390 391 392 393 394 395 396 397 398 |
# File 'lib/pwn/cron.rb', line 389 public_class_method def self.worker_status pid = read_pid running = pid && process_alive?(pid: pid) { running: running ? true : false, pid: running ? pid : nil, pid_file: pid_file, log: worker_log } end |