├── .gitignore ├── README.md ├── app ├── bootstrap.php ├── config.base.php └── console.php ├── bin ├── breakpad_moduleid ├── carburetor ├── dump_syms ├── dump_syms.exe ├── minidump_stackwalk ├── msdia80.dll └── nm ├── cache └── .gitignore ├── composer.json ├── composer.lock ├── dumps └── .gitignore ├── logs └── .gitignore ├── migrations.xml ├── migrations ├── Version20130530032454.php ├── Version20130629035411.php ├── Version20130731153146.php ├── Version20130801013430.php ├── Version20130802024207.php ├── Version20130802030115.php ├── Version20130802030806.php ├── Version20130805150427.php ├── Version20130812022810.php ├── Version20130812032124.php ├── Version20130901024450.php ├── Version20130902133132.php ├── Version20130902135934.php ├── Version20130902151422.php ├── Version20130902230947.php ├── Version20130906055934.php ├── Version20130906201524.php ├── Version20140301232326.php ├── Version20151028144951.php ├── Version20160102155759.php ├── Version20160126125206.php ├── Version20160126155321.php ├── Version20160918195311.php ├── Version20171031224128.php ├── Version20180715160151.php ├── Version20180811185922.php ├── Version20180816153131.php ├── Version20190107164838.php ├── Version20190112160252.php ├── Version20190112173651.php ├── Version20190112195611.php ├── Version20190209155141.php ├── Version20190215233724.php └── Version20190215235846.php ├── munin-plugin ├── scripts ├── clean-sourcemod-symbols.php ├── crashstat.php ├── deduplicate_symbols.php ├── extract_host_error.php ├── get_all_maps.php └── os_versions.php ├── src └── Throttle │ ├── Command │ ├── CrashCleanCommand.php │ ├── CrashProcessCommand.php │ ├── CrashStatsCommand.php │ ├── SymbolsDownloadCommand.php │ ├── SymbolsDumpCommand.php │ ├── SymbolsMozillaDownloadCommand.php │ ├── SymbolsStatsCommand.php │ ├── SymbolsUpdateCommand.php │ └── UserUpdateCommand.php │ ├── Crash.php │ ├── Home.php │ ├── Sharing.php │ ├── Stats.php │ ├── Subscription.php │ └── Symbols.php ├── symbols └── public │ └── .gitignore ├── views ├── carburetor.html.twig ├── console.html.twig ├── dashboard.html.twig ├── details.html.twig ├── error.html.twig ├── index.html.twig ├── invite.html.twig ├── layout.html.twig ├── logs.html.twig ├── share.html.twig ├── stat-counter.stub.twig ├── stats.html.twig ├── submit-empty.txt.twig ├── submit-nosteam.txt.twig ├── submit-reject.txt.twig ├── submit-symbols.txt.twig ├── submit.txt.twig ├── subscribe.html.twig └── view.html.twig └── web ├── css ├── application.css ├── bootstrap-slider.min.css ├── bootstrap.no-icons.min.css ├── font-awesome.min.css └── kindle.css ├── favicon.ico ├── font ├── fontawesome-webfont.eot ├── fontawesome-webfont.svg ├── fontawesome-webfont.ttf └── fontawesome-webfont.woff ├── index.php ├── js ├── bootstrap-slider.min.js ├── bootstrap.min.js ├── carburetor-analyze.js ├── carburetor-memory.js ├── highcharts.data.js ├── highcharts.js ├── jdataview.js ├── jquery.hoverIntent.min.js ├── jquery.min.js └── minidump.js └── robots.txt /.gitignore: -------------------------------------------------------------------------------- 1 | /.wine/ 2 | /app/config.php 3 | /symbols/ 4 | /dumps/ 5 | /logs/ 6 | /vendor/ 7 | /cache/ 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Install Instructions 2 | curl -sS https://getcomposer.org/installer | php 3 | php composer.phar create-project --keep-vcs -s dev asherkin/throttle 4 | cd throttle 5 | cp app/config.base.php app/config.php 6 | vim app/config.php 7 | php app/console.php migrations:migrate 8 | chmod -R a+w logs cache dumps symbols/public 9 | 10 | ## Update Instructions 11 | cd throttle 12 | git pull 13 | php ../composer.phar install 14 | php app/console.php migrations:migrate 15 | rm -rf cache/* 16 | 17 | ## Virtual Host Configuration 18 | 19 | ServerName throttle.example.com 20 | DocumentRoot "/path/to/throttle/web" 21 | 22 | 23 | Options -MultiViews 24 | 25 | RewriteEngine On 26 | RewriteCond %{REQUEST_FILENAME} !-f 27 | RewriteRule ^ index.php [QSA,L] 28 | 29 | 30 | 31 | ## Cron 32 | * * * * * root /var/www/throttle/app/console.php crash:clean > /dev/null; /var/www/throttle/app/console.php crash:process -l 250 -u > /dev/null 33 | 0 * * * * root /var/www/throttle/app/console.php user:update > /dev/null 34 | 15 */3 * * * root /var/www/throttle/app/console.php symbols:update > /dev/null 35 | 30 0 * * * root /var/www/throttle/app/console.php symbols:download > /dev/null 36 | 30 0 * * * root /var/www/throttle/app/console.php symbols:mozilla:download > /dev/null 37 | -------------------------------------------------------------------------------- /app/bootstrap.php: -------------------------------------------------------------------------------- 1 | register(new Silex\Provider\MonologServiceProvider(), array( 18 | 'monolog.logfile' => $app['root'] . '/logs/main.log', 19 | 'monolog.handler' => new Monolog\Handler\StreamHandler($app['root'] . '/logs/main.log', Monolog\Logger::NOTICE), 20 | 'monolog.level' => Monolog\Logger::DEBUG, 21 | 'monolog.name' => 'throttle', 22 | )); 23 | 24 | try { 25 | $app['config'] = include_once __DIR__ . '/config.php'; 26 | } catch (ErrorException $e) { 27 | $app['config'] = false; 28 | return $app; 29 | } 30 | 31 | $app['debug'] = $app['config']['debug']; 32 | 33 | $app['monolog'] = $app->share($app->extend('monolog', function($monolog, $app) { 34 | if ($app['config']['email-errors']) { 35 | $monolog->pushHandler(new Monolog\Handler\NativeMailerHandler( 36 | $app['config']['email-errors.to'], 37 | '[Throttle] Error Report', 38 | $app['config']['email-errors.from'], 39 | Monolog\Logger::CRITICAL 40 | )); 41 | } 42 | 43 | return $monolog; 44 | })); 45 | 46 | $app->register(new Silex\Provider\DoctrineServiceProvider(), array( 47 | 'db.options' => array( 48 | 'driver' => 'pdo_mysql', 49 | 'host' => $app['config']['db.host'], 50 | 'user' => $app['config']['db.user'], 51 | 'password' => $app['config']['db.password'], 52 | 'dbname' => $app['config']['db.name'], 53 | 'charset' => 'utf8', 54 | 'driverOptions' => array( 55 | 1002 => 'SET NAMES utf8', 56 | ), 57 | ), 58 | )); 59 | 60 | $app['redis'] = $app->share(function() use ($app) { 61 | $redis = new \Redis(); 62 | $redis->connect('127.0.0.1', 6379, 1); 63 | return $redis; 64 | }); 65 | 66 | /* 67 | $app['queue'] = $app->share(function() use ($app) { 68 | return new Pheanstalk\Pheanstalk('127.0.0.1'); 69 | }); 70 | */ 71 | 72 | return $app; 73 | 74 | -------------------------------------------------------------------------------- /app/config.base.php: -------------------------------------------------------------------------------- 1 | false, 14 | 'maintenance' => false, 15 | 'show-version' => true, 16 | 17 | 'email-errors' => false, 18 | 'email-errors.from' => 'noreply@example.com', 19 | 'email-errors.to' => array( 20 | 'webmaster@example.com', 21 | ), 22 | 23 | 'db.host' => 'localhost', 24 | 'db.user' => 'root', 25 | 'db.password' => '', 26 | 'db.name' => 'throttle', 27 | 28 | 'hostname' => 'throttle.example.com', 29 | 'trusted-proxies' => array(), 30 | 31 | 'admins' => array(), 32 | 'developers' => array(), 33 | 34 | 'apikey' => false, 35 | 'accelerator' => false, 36 | 37 | 'show-version' => true, 38 | 39 | 'symbol-stores' => array(), 40 | )); 41 | 42 | -------------------------------------------------------------------------------- /app/console.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | register(new Cilex\Provider\Console\Adapter\Silex\ConsoleServiceProvider(), array( 7 | 'console.name' => 'Throttle', 8 | 'console.version' => '0.0.0', 9 | )); 10 | 11 | $output = new \Symfony\Component\Console\Output\ConsoleOutput(); 12 | 13 | if ($app['config'] === false) { 14 | return $app['console']->renderException(new \Exception('Missing configuration file, please see app/config.base.php'), $output); 15 | } 16 | 17 | $commands = id(new \FileFinder($app['root'] . '/src/Throttle/Command/'))->withType('f')->withSuffix('php')->find(); 18 | 19 | foreach ($commands as &$command) { 20 | $command = '\\Throttle\\Command\\' . substr($command, 0, -4); 21 | $command = new $command; 22 | } 23 | unset($command); 24 | 25 | $app['console']->addCommands($commands); 26 | 27 | $app['console']->getHelperSet()->set(new \Doctrine\DBAL\Tools\Console\Helper\ConnectionHelper($app['db']), 'db'); 28 | 29 | $app['console']->addCommands(array( 30 | new \Doctrine\DBAL\Migrations\Tools\Console\Command\ExecuteCommand, 31 | new \Doctrine\DBAL\Migrations\Tools\Console\Command\GenerateCommand, 32 | new \Doctrine\DBAL\Migrations\Tools\Console\Command\MigrateCommand, 33 | new \Doctrine\DBAL\Migrations\Tools\Console\Command\StatusCommand, 34 | new \Doctrine\DBAL\Migrations\Tools\Console\Command\VersionCommand, 35 | )); 36 | 37 | $app['console']->run(null, $output); 38 | 39 | -------------------------------------------------------------------------------- /bin/breakpad_moduleid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asherkin/throttle/f150edb423ce2f805b7c20d21dcc6887b9128cd1/bin/breakpad_moduleid -------------------------------------------------------------------------------- /bin/carburetor: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asherkin/throttle/f150edb423ce2f805b7c20d21dcc6887b9128cd1/bin/carburetor -------------------------------------------------------------------------------- /bin/dump_syms: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asherkin/throttle/f150edb423ce2f805b7c20d21dcc6887b9128cd1/bin/dump_syms -------------------------------------------------------------------------------- /bin/dump_syms.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asherkin/throttle/f150edb423ce2f805b7c20d21dcc6887b9128cd1/bin/dump_syms.exe -------------------------------------------------------------------------------- /bin/minidump_stackwalk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asherkin/throttle/f150edb423ce2f805b7c20d21dcc6887b9128cd1/bin/minidump_stackwalk -------------------------------------------------------------------------------- /bin/msdia80.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asherkin/throttle/f150edb423ce2f805b7c20d21dcc6887b9128cd1/bin/msdia80.dll -------------------------------------------------------------------------------- /bin/nm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asherkin/throttle/f150edb423ce2f805b7c20d21dcc6887b9128cd1/bin/nm -------------------------------------------------------------------------------- /cache/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asherkin/throttle/f150edb423ce2f805b7c20d21dcc6887b9128cd1/cache/.gitignore -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "asherkin/throttle", 3 | "type": "project", 4 | "description": "Lightweight google-breakpad back-end written in PHP and backed by MySQL", 5 | "homepage": "https://crash.limetech.org/", 6 | "license": "Apache-2.0", 7 | "authors": [ 8 | { 9 | "name": "Asher Baker", 10 | "email": "asherkin@limetech.io", 11 | "homepage": "https://limetech.io" 12 | } 13 | ], 14 | "repositories": [ 15 | { 16 | "type": "package", 17 | "package": { 18 | "name": "phacility/libphutil", 19 | "version": "dev-stable", 20 | "source": { 21 | "url": "https://github.com/phacility/libphutil.git", 22 | "type": "git", 23 | "reference": "origin/stable" 24 | } 25 | } 26 | } 27 | ], 28 | "require": { 29 | "silex/silex": "~1.1.0", 30 | "symfony/monolog-bridge": "~2.3.0", 31 | "monolog/monolog": "~1.0", 32 | "symfony/doctrine-bridge": "~2.3.0", 33 | "doctrine/dbal": "~2.2", 34 | "doctrine/migrations": "~1.5.0", 35 | "symfony/twig-bridge": "~2.3.0", 36 | "twig/twig": "~1.8", 37 | "cilex/console-service-provider": "~1.0.0", 38 | "symfony/console": "2.3.38", 39 | "silex/web-profiler": "1.0.2", 40 | "symfony/web-profiler-bundle": "~2.3.0", 41 | "symfony/debug": "~2.3.0", 42 | "phacility/libphutil": "dev-stable", 43 | "lightopenid/lightopenid": "dev-master", 44 | "pda/pheanstalk": "^3.1", 45 | "sorien/silex-dbal-profiler": "~1.1" 46 | }, 47 | "autoload": { 48 | "psr-0": { "Throttle": "src/" } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /dumps/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asherkin/throttle/f150edb423ce2f805b7c20d21dcc6887b9128cd1/dumps/.gitignore -------------------------------------------------------------------------------- /logs/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asherkin/throttle/f150edb423ce2f805b7c20d21dcc6887b9128cd1/logs/.gitignore -------------------------------------------------------------------------------- /migrations.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | Throttle Migrations 6 | ThrottleMigrations 7 | 8 | migrations 9 | 10 | -------------------------------------------------------------------------------- /migrations/Version20130530032454.php: -------------------------------------------------------------------------------- 1 | createTable('crash'); 13 | 14 | $crash->addColumn('id', 'string', array('length' => 12, 'fixed' => true)); 15 | $crash->addColumn('timestamp', 'datetime'); 16 | $crash->addColumn('ip', 'integer', array('unsigned' => true)); 17 | $crash->addColumn('owner', 'string', array('length' => 255, 'notnull' => false)); 18 | $crash->addColumn('metadata', 'string', array('length' => 4095, 'notnull' => false)); 19 | $crash->addColumn('cmdline', 'string', array('length' => 4095, 'notnull' => false)); 20 | $crash->addColumn('thread', 'integer', array('notnull' => false)); 21 | $crash->addColumn('processed', 'boolean', array('default' => false)); 22 | 23 | $crash->setPrimaryKey(array('id')); 24 | 25 | $crash->addIndex(array('owner')); 26 | $crash->addIndex(array('processed')); 27 | 28 | $frame = $schema->createTable('frame'); 29 | 30 | $frame->addColumn('crash', 'string', array('length' => 12, 'fixed' => true)); 31 | $frame->addColumn('thread', 'integer'); 32 | $frame->addColumn('frame', 'integer'); 33 | $frame->addColumn('module', 'string', array('length' => 255)); 34 | $frame->addColumn('function', 'string', array('length' => 255)); 35 | $frame->addColumn('file', 'string', array('length' => 255)); 36 | $frame->addColumn('line', 'string', array('length' => 255)); 37 | $frame->addColumn('offset', 'string', array('length' => 255)); 38 | 39 | $frame->setPrimaryKey(array('crash', 'thread', 'frame')); 40 | 41 | $frame->addIndex(array('crash')); 42 | $frame->addIndex(array('crash', 'thread')); 43 | 44 | $frame->addForeignKeyConstraint($crash, array('crash'), array('id'), array('onUpdate' => 'CASCADE', 'onDelete' => 'CASCADE')); 45 | 46 | $module = $schema->createTable('module'); 47 | 48 | $module->addColumn('crash', 'string', array('length' => 12, 'fixed' => true)); 49 | $module->addColumn('name', 'string', array('length' => 255)); 50 | $module->addColumn('identifier', 'string', array('length' => 255)); 51 | 52 | $module->setPrimaryKey(array('crash', 'name', 'identifier')); 53 | 54 | $module->addIndex(array('crash')); 55 | 56 | $module->addForeignKeyConstraint($crash, array('crash'), array('id'), array('onUpdate' => 'CASCADE', 'onDelete' => 'CASCADE')); 57 | } 58 | 59 | public function down(Schema $schema) 60 | { 61 | $schema->dropTable('module'); 62 | $schema->dropTable('frame'); 63 | $schema->dropTable('crash'); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /migrations/Version20130629035411.php: -------------------------------------------------------------------------------- 1 | getTable('crash'); 13 | 14 | $crash->addColumn('failed', 'boolean', array('default' => false)); 15 | } 16 | 17 | public function down(Schema $schema) 18 | { 19 | $crash = $schema->getTable('crash'); 20 | 21 | $crash->dropColumn('failed'); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /migrations/Version20130731153146.php: -------------------------------------------------------------------------------- 1 | addSql('UPDATE crash SET owner = NULL WHERE owner = 0'); 13 | $this->addSql('ALTER TABLE crash CHANGE owner owner BIGINT UNSIGNED NULL DEFAULT NULL'); 14 | 15 | $crash = $schema->createTable('user'); 16 | 17 | $crash->addColumn('id', 'bigint', array('unsigned' => true)); 18 | $crash->addColumn('name', 'string', array('length' => 255, 'notnull' => false)); 19 | $crash->addColumn('avatar', 'string', array('length' => 255, 'notnull' => false)); 20 | $crash->addColumn('updated', 'datetime', array('notnull' => false)); 21 | 22 | $crash->setPrimaryKey(array('id')); 23 | 24 | $crash->addIndex(array('updated')); 25 | } 26 | 27 | public function down(Schema $schema) 28 | { 29 | $schema->dropTable('user'); 30 | 31 | $this->addSql('ALTER TABLE crash CHANGE owner owner VARCHAR(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /migrations/Version20130801013430.php: -------------------------------------------------------------------------------- 1 | addSql('INSERT IGNORE INTO user SELECT DISTINCT owner, NULL, NULL, NULL FROM crash WHERE owner IS NOT NULL'); 13 | } 14 | 15 | public function down(Schema $schema) 16 | { 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /migrations/Version20130802024207.php: -------------------------------------------------------------------------------- 1 | createTable('server'); 13 | 14 | $server->addColumn('owner', 'bigint', array('unsigned' => true)); 15 | $server->addColumn('id', 'string', array('length' => 64, 'default' => '')); 16 | 17 | $server->setPrimaryKey(array('owner', 'id')); 18 | 19 | $user = $schema->getTable('user'); 20 | $server->addForeignKeyConstraint($user, array('owner'), array('id'), array('onUpdate' => 'CASCADE', 'onDelete' => 'CASCADE')); 21 | 22 | $crash = $schema->getTable('crash'); 23 | $crash->addColumn('server', 'string', array('length' => 64, 'notnull' => false)); 24 | } 25 | 26 | public function down(Schema $schema) 27 | { 28 | $crash = $schema->getTable('crash'); 29 | $crash->dropColumn('server'); 30 | 31 | $schema->dropTable('server'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /migrations/Version20130802030115.php: -------------------------------------------------------------------------------- 1 | addSql('INSERT IGNORE INTO server SELECT id, \'\' FROM user'); 13 | $this->addSql('UPDATE crash SET server = \'\' WHERE owner IS NOT NULL'); 14 | } 15 | 16 | public function down(Schema $schema) 17 | { 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /migrations/Version20130802030806.php: -------------------------------------------------------------------------------- 1 | getTable('crash'); 13 | 14 | $crash->addIndex(array('owner', 'server')); 15 | 16 | $server = $schema->getTable('server'); 17 | $crash->addForeignKeyConstraint($server, array('owner', 'server'), array('owner', 'id'), array('onUpdate' => 'CASCADE', 'onDelete' => 'CASCADE')); 18 | } 19 | 20 | public function down(Schema $schema) 21 | { 22 | $crash = $schema->getTable('crash'); 23 | 24 | $crash->removeForeignKey('FK_D7E8F0DFCF60E67C5A6DD5F6'); 25 | $crash->dropIndex('IDX_D7E8F0DFCF60E67C5A6DD5F6'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /migrations/Version20130805150427.php: -------------------------------------------------------------------------------- 1 | getTable('crash'); 13 | $crash->addIndex(array('timestamp')); 14 | } 15 | 16 | public function down(Schema $schema) 17 | { 18 | $crash = $schema->getTable('crash'); 19 | $crash->dropIndex('IDX_D7E8F0DFA5D6E63E'); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /migrations/Version20130812022810.php: -------------------------------------------------------------------------------- 1 | getTable('frame'); 13 | $frame->addColumn('rendered', 'string', array('length' => 512, 'notnull' => false)); 14 | } 15 | 16 | public function down(Schema $schema) 17 | { 18 | $frame = $schema->getTable('frame'); 19 | $frame->dropColumn('rendered'); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /migrations/Version20130812032124.php: -------------------------------------------------------------------------------- 1 | addSql('UPDATE frame SET rendered = IF(file != \'\', CONCAT(module, \'!\', function, \' [\', SUBSTRING_INDEX(REPLACE(file, \'\\\\\', \'/\'), \'/\', -1), \':\', line, \' + \', offset, \']\'), IF(function != \'\', CONCAT(module, \'!\', function, \' + \', offset), IF(module != \'\', CONCAT(module, \' + \', offset), offset))) WHERE rendered IS NULL'); 13 | } 14 | 15 | public function down(Schema $schema) 16 | { 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /migrations/Version20130901024450.php: -------------------------------------------------------------------------------- 1 | getTable('module'); 13 | $module->addIndex(array('name', 'identifier')); 14 | } 15 | 16 | public function down(Schema $schema) 17 | { 18 | $module = $schema->getTable('module'); 19 | $module->dropIndex('IDX_C2426285E237E06772E836A'); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /migrations/Version20130902133132.php: -------------------------------------------------------------------------------- 1 | getTable('module'); 13 | $module->addColumn('processed', 'boolean', array('default' => false)); 14 | $module->addColumn('present', 'boolean', array('default' => false)); 15 | } 16 | 17 | public function down(Schema $schema) 18 | { 19 | $module = $schema->getTable('module'); 20 | $module->dropColumn('processed'); 21 | $module->dropColumn('present'); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /migrations/Version20130902135934.php: -------------------------------------------------------------------------------- 1 | addSql('UPDATE module SET processed = 1'); 13 | } 14 | 15 | public function down(Schema $schema) 16 | { 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /migrations/Version20130902151422.php: -------------------------------------------------------------------------------- 1 | getTable('module'); 13 | $module->addIndex(array('processed', 'present')); 14 | } 15 | 16 | public function down(Schema $schema) 17 | { 18 | $module = $schema->getTable('module'); 19 | $module->dropIndex('IDX_C24262827FB1B8BFDBCAE17'); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /migrations/Version20130902230947.php: -------------------------------------------------------------------------------- 1 | getTable('crash'); 13 | $crash->addColumn('output', 'text', array('notnull' => false)); 14 | } 15 | 16 | public function down(Schema $schema) 17 | { 18 | $crash = $schema->getTable('crash'); 19 | $crash->dropColumn('output'); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /migrations/Version20130906055934.php: -------------------------------------------------------------------------------- 1 | createTable('notice'); 13 | 14 | $notice->addColumn('id', 'string', array('length' => 255)); 15 | $notice->addColumn('severity', 'string', array('length' => 255)); 16 | $notice->addColumn('text', 'string', array('length' => 4095)); 17 | 18 | $notice->setPrimaryKey(array('id')); 19 | 20 | $crashnotice = $schema->createTable('crashnotice'); 21 | 22 | $crashnotice->addColumn('crash', 'string', array('length' => 12, 'fixed' => true)); 23 | $crashnotice->addColumn('notice', 'string', array('length' => 255)); 24 | 25 | $crashnotice->setPrimaryKey(array('crash', 'notice')); 26 | 27 | $crashnotice->addIndex(array('crash')); 28 | 29 | $crash = $schema->getTable('crash'); 30 | $crashnotice->addForeignKeyConstraint($crash, array('crash'), array('id'), array('onUpdate' => 'CASCADE', 'onDelete' => 'CASCADE')); 31 | $crashnotice->addForeignKeyConstraint($notice, array('notice'), array('id'), array('onUpdate' => 'CASCADE', 'onDelete' => 'CASCADE')); 32 | } 33 | 34 | public function down(Schema $schema) 35 | { 36 | $schema->dropTable('crashnotice'); 37 | $schema->dropTable('notice'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /migrations/Version20130906201524.php: -------------------------------------------------------------------------------- 1 | getTable('notice'); 13 | 14 | $notice->addColumn('rule', 'string', array('length' => 4095)); 15 | } 16 | 17 | public function down(Schema $schema) 18 | { 19 | $notice = $schema->getTable('notice'); 20 | 21 | $notice->dropColumn('rule'); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /migrations/Version20140301232326.php: -------------------------------------------------------------------------------- 1 | getTable('crash'); 13 | $crash->dropColumn('output'); 14 | } 15 | 16 | public function down(Schema $schema) 17 | { 18 | $crash = $schema->getTable('crash'); 19 | $crash->addColumn('output', 'text', array('notnull' => false)); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /migrations/Version20151028144951.php: -------------------------------------------------------------------------------- 1 | getTable('frame'); 13 | $frame->addColumn('url', 'string', array('length' => 1024, 'notnull' => false)); 14 | } 15 | 16 | public function down(Schema $schema) 17 | { 18 | $frame = $schema->getTable('frame'); 19 | $frame->dropColumn('url'); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /migrations/Version20160102155759.php: -------------------------------------------------------------------------------- 1 | getTable('module'); 13 | $module->addColumn('base', 'integer', array('unsigned' => true, 'notnull' => false)); 14 | 15 | $frame = $schema->getTable('frame'); 16 | $frame->addColumn('address', 'integer', array('unsigned' => true, 'notnull' => false)); 17 | } 18 | 19 | public function down(Schema $schema) 20 | { 21 | $module = $schema->getTable('module'); 22 | $module->dropColumn('base'); 23 | 24 | $frame = $schema->getTable('frame'); 25 | $frame->dropColumn('address'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /migrations/Version20160126125206.php: -------------------------------------------------------------------------------- 1 | getTable('crash'); 13 | $crash->addColumn('stackhash', 'string', array('length' => 80, 'notnull' => false)); 14 | 15 | $crash->addIndex(array('stackhash')); 16 | } 17 | 18 | public function down(Schema $schema) 19 | { 20 | $crash = $schema->getTable('crash'); 21 | $crash->dropColumn('stackhash'); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /migrations/Version20160126155321.php: -------------------------------------------------------------------------------- 1 | addSql('UPDATE crash, (SELECT crash, thread, GROUP_CONCAT(SUBSTRING(SHA2(rendered, 256), 1, 8) ORDER BY frame ASC SEPARATOR \'\') AS hash FROM frame WHERE frame < 10 AND module != \'\' GROUP BY crash, thread) AS hashes SET crash.stackhash = hashes.hash WHERE crash.id = hashes.crash AND crash.thread = hashes.thread AND crash.processed = 1 AND crash.failed = 0'); 13 | } 14 | 15 | public function down(Schema $schema) 16 | { 17 | $this->addSql('UPDATE crash SET crash.stackhash = NULL'); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /migrations/Version20160918195311.php: -------------------------------------------------------------------------------- 1 | addSql('ALTER TABLE crash CHANGE ip ip VARBINARY(16) DEFAULT NULL'); 13 | $this->addSql('UPDATE crash SET ip = INET6_ATON(INET_NTOA(ip))'); 14 | } 15 | 16 | public function down(Schema $schema) 17 | { 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /migrations/Version20171031224128.php: -------------------------------------------------------------------------------- 1 | createTable('subscription'); 13 | 14 | $subscription->addColumn('subscription_id', 'string', array('length' => 255)); 15 | $subscription->addColumn('steamid', 'bigint', array('unsigned' => true, 'notnull' => false)); 16 | $subscription->addColumn('update_date', 'datetime', array()); 17 | $subscription->addColumn('status', 'string', array('length' => 255, 'notnull' => false)); 18 | $subscription->addColumn('start_date', 'datetime', array('notnull' => false)); 19 | $subscription->addColumn('end_date', 'datetime', array('notnull' => false)); 20 | $subscription->addColumn('checkout_id', 'string', array('length' => 255, 'notnull' => false)); 21 | $subscription->addColumn('update_url', 'string', array('length' => 511, 'notnull' => false)); 22 | $subscription->addColumn('cancel_url', 'string', array('length' => 511, 'notnull' => false)); 23 | $subscription->addColumn('user_id', 'string', array('length' => 255, 'notnull' => false)); 24 | $subscription->addColumn('plan_id', 'string', array('length' => 255, 'notnull' => false)); 25 | $subscription->addColumn('passthrough', 'string', array('length' => 255, 'notnull' => false)); 26 | 27 | $subscription->setPrimaryKey(array('subscription_id')); 28 | 29 | $user = $schema->getTable('user'); 30 | $subscription->addForeignKeyConstraint($user, array('steamid'), array('id'), array('onUpdate' => 'CASCADE', 'onDelete' => 'SET NULL')); 31 | 32 | $payment = $schema->createTable('payment'); 33 | 34 | $payment->addColumn('order_id', 'string', array('length' => 255)); 35 | $payment->addColumn('subscription_id', 'string', array('length' => 255)); 36 | $payment->addColumn('payment_date', 'datetime', array('notnull' => false)); 37 | $payment->addColumn('refund_date', 'datetime', array('notnull' => false)); 38 | $payment->addColumn('receipt_url', 'string', array('length' => 511, 'notnull' => false)); 39 | $payment->addColumn('gross_amount', 'integer', array('unsigned' => true, 'notnull' => false)); 40 | $payment->addColumn('fee_amount', 'integer', array('unsigned' => true, 'notnull' => false)); 41 | $payment->addColumn('tax_amount', 'integer', array('unsigned' => true, 'notnull' => false)); 42 | $payment->addColumn('earned_amount', 'integer', array('unsigned' => true, 'notnull' => false)); 43 | 44 | $payment->setPrimaryKey(array('order_id')); 45 | 46 | $payment->addForeignKeyConstraint($subscription, array('subscription_id'), array('subscription_id'), array('onUpdate' => 'CASCADE', 'onDelete' => 'CASCADE')); 47 | } 48 | 49 | public function down(Schema $schema) 50 | { 51 | $schema->dropTable('subscription'); 52 | $schema->dropTable('payment'); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /migrations/Version20180715160151.php: -------------------------------------------------------------------------------- 1 | createTable('share'); 13 | 14 | $share->addColumn('owner', 'bigint', array('unsigned' => true)); 15 | $share->addColumn('user', 'bigint', array('unsigned' => true)); 16 | $share->addColumn('accepted', 'datetime', array('notnull' => false)); 17 | 18 | $share->setPrimaryKey(array('owner', 'user')); 19 | 20 | $user = $schema->getTable('user'); 21 | $share->addForeignKeyConstraint($user, array('owner'), array('id'), array('onUpdate' => 'CASCADE', 'onDelete' => 'CASCADE')); 22 | $share->addForeignKeyConstraint($user, array('user'), array('id'), array('onUpdate' => 'CASCADE', 'onDelete' => 'CASCADE')); 23 | } 24 | 25 | public function down(Schema $schema) 26 | { 27 | $schema->dropTable('share'); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /migrations/Version20180811185922.php: -------------------------------------------------------------------------------- 1 | addSql('ALTER TABLE frame CHANGE rendered rendered VARCHAR(1024) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL'); 13 | } 14 | 15 | public function down(Schema $schema) 16 | { 17 | $this->throwIrreversibleMigrationException(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /migrations/Version20180816153131.php: -------------------------------------------------------------------------------- 1 | addSql('ALTER TABLE frame CHANGE function function VARCHAR(1024) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL'); 13 | } 14 | 15 | public function down(Schema $schema) 16 | { 17 | $this->throwIrreversibleMigrationException(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /migrations/Version20190107164838.php: -------------------------------------------------------------------------------- 1 | getTable('crash'); 13 | $crash->addColumn('lastview', 'datetime', array('notnull' => false)); 14 | 15 | $crash->addIndex(array('lastview')); 16 | } 17 | 18 | public function down(Schema $schema) 19 | { 20 | $crash = $schema->getTable('crash'); 21 | $crash->dropColumn('lastview'); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /migrations/Version20190112160252.php: -------------------------------------------------------------------------------- 1 | getTable('crash'); 16 | $crash->dropIndex('IDX_D7E8F0DFCF60E67C'); 17 | 18 | $crashnotice = $schema->getTable('crashnotice'); 19 | $crashnotice->dropIndex('IDX_785CF107D7E8F0DF'); 20 | 21 | $server = $schema->getTable('server'); 22 | $server->dropIndex('IDX_5A6DD5F6CF60E67C'); 23 | 24 | $share = $schema->getTable('share'); 25 | $share->dropIndex('IDX_EF069D5ACF60E67C'); 26 | } 27 | 28 | /** 29 | * @param Schema $schema 30 | */ 31 | public function down(Schema $schema) 32 | { 33 | $crash = $schema->getTable('crash'); 34 | $crash->addIndex(array('owner')); 35 | 36 | $crashnotice = $schema->getTable('crashnotice'); 37 | $crashnotice->addIndex(array('crash')); 38 | 39 | $server = $schema->getTable('server'); 40 | $server->addIndex(array('owner')); 41 | 42 | $share = $schema->getTable('share'); 43 | $share->addIndex(array('owner')); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /migrations/Version20190112173651.php: -------------------------------------------------------------------------------- 1 | getTable('crash'); 16 | $crash->removeForeignKey('FK_D7E8F0DFCF60E67C5A6DD5F6'); 17 | $crash->dropColumn('server'); 18 | 19 | $user = $schema->getTable('user'); 20 | $crash->addForeignKeyConstraint($user, array('owner'), array('id'), array('onUpdate' => 'CASCADE', 'onDelete' => 'CASCADE')); 21 | $crash->dropIndex('IDX_D7E8F0DFCF60E67C5A6DD5F6'); 22 | 23 | $schema->dropTable('server'); 24 | } 25 | 26 | /** 27 | * @param Schema $schema 28 | */ 29 | public function down(Schema $schema) 30 | { 31 | $this->throwIrreversibleMigrationException(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /migrations/Version20190112195611.php: -------------------------------------------------------------------------------- 1 | getTable('crash'); 16 | $crash->addIndex(array('ip')); 17 | $crash->addIndex(array('failed')); 18 | 19 | $frame = $schema->getTable('frame'); 20 | $frame->dropIndex('IDX_B5F83CCDD7E8F0DF'); 21 | $frame->dropIndex('IDX_B5F83CCDD7E8F0DF31204C83'); 22 | $frame->dropColumn('address'); 23 | 24 | $module = $schema->getTable('module'); 25 | $module->dropIndex('IDX_C242628D7E8F0DF'); 26 | $module->dropIndex('IDX_C24262827FB1B8BFDBCAE17'); 27 | $module->addIndex(array('processed')); 28 | $module->addIndex(array('present')); 29 | } 30 | 31 | /** 32 | * @param Schema $schema 33 | */ 34 | public function down(Schema $schema) 35 | { 36 | $this->throwIrreversibleMigrationException(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /migrations/Version20190209155141.php: -------------------------------------------------------------------------------- 1 | getTable('user'); 13 | $user->addColumn('lastactive', 'datetime', array('notnull' => false)); 14 | } 15 | 16 | public function down(Schema $schema) 17 | { 18 | $user = $schema->getTable('user'); 19 | $user->dropColumn('lastactive'); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /migrations/Version20190215233724.php: -------------------------------------------------------------------------------- 1 | getTable('crash'); 13 | 14 | $crash->addColumn('crashmodule', 'string', array('length' => 255, 'notnull' => false)); 15 | $crash->addColumn('crashfunction', 'string', array('length' => 769, 'notnull' => false)); 16 | 17 | $crash->addIndex(array('crashmodule', 'crashfunction')); 18 | } 19 | 20 | public function down(Schema $schema) 21 | { 22 | $crash = $schema->getTable('crash'); 23 | 24 | $crash->dropColumn('crashmodule'); 25 | $crash->dropColumn('crashfunction'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /migrations/Version20190215235846.php: -------------------------------------------------------------------------------- 1 | addSql('UPDATE crash JOIN frame ON frame.crash = crash.id AND frame.thread = crash.thread AND frame.frame = 0 SET crashmodule = module, crashfunction = COALESCE(NULLIF(function, \'\'), offset)'); 13 | } 14 | 15 | public function down(Schema $schema) 16 | { 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /munin-plugin: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 1 && $argv[1] === 'config') { 5 | echo <<<'EOT' 6 | multigraph throttle_submitted 7 | graph_title Crashes submitted 8 | graph_period minute 9 | graph_args --lower-limit 0 10 | graph_vlabel crashes per ${graph_period} 11 | graph_category throttle 12 | graph_order accepted rejected_no_minidump rejected_no_steam rejected_rate_limit presubmitted submitted 13 | presubmitted.label presubmitted 14 | presubmitted.type DERIVE 15 | presubmitted.min 0 16 | presubmitted.colour COLOUR4 17 | submitted.label submitted 18 | submitted.type DERIVE 19 | submitted.min 0 20 | submitted.colour 000000 21 | accepted.label accepted 22 | accepted.type DERIVE 23 | accepted.min 0 24 | accepted.colour COLOUR0 25 | accepted.draw AREASTACK 26 | rejected_no_minidump.label rejected: no minidump 27 | rejected_no_minidump.type DERIVE 28 | rejected_no_minidump.min 0 29 | rejected_no_minidump.colour COLOUR1 30 | rejected_no_minidump.draw AREASTACK 31 | rejected_no_steam.label rejected: no-steam 32 | rejected_no_steam.type DERIVE 33 | rejected_no_steam.min 0 34 | rejected_no_steam.colour COLOUR2 35 | rejected_no_steam.draw AREASTACK 36 | rejected_rate_limit.label rejected: rate limited 37 | rejected_rate_limit.type DERIVE 38 | rejected_rate_limit.min 0 39 | rejected_rate_limit.colour COLOUR3 40 | rejected_rate_limit.draw AREASTACK 41 | multigraph throttle_processed 42 | graph_title Crashes processed 43 | graph_period minute 44 | graph_args --lower-limit 0 45 | graph_vlabel crashes per ${graph_period} 46 | graph_category throttle 47 | graph_order failed processed needs_reprocessing 48 | processed.label processed 49 | processed.type DERIVE 50 | processed.min 0 51 | processed.colour COLOUR0 52 | processed.draw AREASTACK 53 | failed.label failed 54 | failed.type DERIVE 55 | failed.min 0 56 | failed.colour COLOUR2 57 | failed.draw AREASTACK 58 | needs_reprocessing.label marked for reprocessing 59 | needs_reprocessing.type DERIVE 60 | needs_reprocessing.min 0 61 | needs_reprocessing.colour COLOUR1 62 | multigraph throttle_cleaned 63 | graph_title Crashes cleaned 64 | graph_period minute 65 | graph_args --lower-limit 0 66 | graph_vlabel crashes per ${graph_period} 67 | graph_category throttle 68 | graph_order limit old orphan 69 | limit.label reached user limit 70 | limit.type DERIVE 71 | limit.min 0 72 | limit.draw AREASTACK 73 | old.label reached age limit 74 | old.type DERIVE 75 | old.min 0 76 | old.draw AREASTACK 77 | orphan.label orphaned minidumps 78 | orphan.type DERIVE 79 | orphan.min 0 80 | orphan.colour 000000 81 | multigraph throttle_symbols 82 | graph_title Symbol lookups 83 | graph_period minute 84 | graph_args --lower-limit 0 85 | graph_vlabel lookups per ${graph_period} 86 | graph_category throttle 87 | graph_order missing_memory missing found cached found_memory 88 | missing_memory.label missing cached 89 | missing_memory.type DERIVE 90 | missing_memory.min 0 91 | missing_memory.colour COLOUR3 92 | missing_memory.draw AREASTACK 93 | missing.label missing 94 | missing.type DERIVE 95 | missing.min 0 96 | missing.colour COLOUR2 97 | missing.draw AREASTACK 98 | found.label found compressed 99 | found.type DERIVE 100 | found.min 0 101 | found.colour COLOUR0 102 | found.draw AREASTACK 103 | cached.label found 104 | cached.type DERIVE 105 | cached.min 0 106 | cached.colour COLOUR1 107 | cached.draw AREASTACK 108 | found_memory.label found cached 109 | found_memory.type DERIVE 110 | found_memory.min 0 111 | found_memory.colour COLOUR5 112 | found_memory.draw AREASTACK 113 | multigraph throttle_repocache 114 | graph_title Symbol repo cache 115 | graph_period minute 116 | graph_args --lower-limit 0 117 | graph_vlabel lookups per ${graph_period} 118 | graph_category throttle 119 | graph_order hit miss 120 | miss.label miss 121 | miss.type DERIVE 122 | miss.min 0 123 | miss.colour COLOUR2 124 | miss.draw AREASTACK 125 | hit.label hit 126 | hit.type DERIVE 127 | hit.min 0 128 | hit.colour COLOUR0 129 | hit.draw AREASTACK 130 | multigraph throttle_userupdate 131 | graph_title Users updated 132 | graph_period minute 133 | graph_args --lower-limit 0 134 | graph_vlabel users per ${graph_period} 135 | graph_category throttle 136 | graph_order failed updated 137 | updated.label updated 138 | updated.type DERIVE 139 | updated.min 0 140 | updated.colour COLOUR0 141 | updated.draw AREASTACK 142 | failed.label failed 143 | failed.type DERIVE 144 | failed.min 0 145 | failed.colour COLOUR2 146 | failed.draw AREASTACK 147 | multigraph throttle_processingtime 148 | graph_title Processing duration 149 | graph_category throttle 150 | graph_vlabel duration (seconds) 151 | min.label min 152 | avg.label average 153 | max.label max 154 | 155 | EOT; 156 | 157 | exit(); 158 | } 159 | 160 | $redis = new \Redis(); 161 | $redis->connect('127.0.0.1', 6379, 1); 162 | 163 | $stats = $redis->hGetAll('throttle:stats'); 164 | 165 | //print_r($stats); 166 | 167 | list($times) = $redis->multi() 168 | ->lRange('throttle:stats:processing', 0, -1) 169 | ->del('throttle:stats:processing', 0, 0) 170 | ->exec(); 171 | 172 | $time_values = array_map(function($time) { 173 | return (float)substr($time, strpos($time, ':') + 1); 174 | }, $times); 175 | 176 | $time_min = 'U'; 177 | $time_avg = 'U'; 178 | $time_max = 'U'; 179 | 180 | if (!empty($time_values)) { 181 | $time_min = min($time_values); 182 | $time_avg = array_sum($time_values) / count($time_values); 183 | $time_max = max($time_values); 184 | } 185 | 186 | $redis->close(); 187 | 188 | echo << 1) { 8 | $root = $argv[1]; 9 | } 10 | 11 | if (!is_dir($root)) { 12 | error_log('Not a directory: ' . $root); 13 | exit(); 14 | } 15 | 16 | $used_file = './used_symbols.txt'; 17 | if ($argc > 2) { 18 | $used_file = $argv[2]; 19 | } 20 | 21 | if (!is_readable($used_file)) { 22 | error_log('Not readable: ' . $used_file); 23 | exit(); 24 | } 25 | 26 | $used = file($used_file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); 27 | $usedMap = []; 28 | foreach ($used as $line) { 29 | $line = preg_split('/\\s+/', $line, null, PREG_SPLIT_NO_EMPTY); 30 | 31 | if ($line[1] == '(deleted)') { 32 | continue; 33 | } 34 | 35 | if (count($line) !== 2) { 36 | error_log('Failed to parse used_symbols, too many: ' . print_r($line, true)); 37 | exit(); 38 | } 39 | 40 | if ($line[1] == '000000000000000000000000000000000') { 41 | continue; 42 | } 43 | 44 | list($name, $identifier) = $line; 45 | 46 | if (!isset($usedMap[$name])) { 47 | $usedMap[$name] = []; 48 | } 49 | 50 | $usedMap[$name][$identifier] = true; 51 | } 52 | 53 | if (substr($root, -1) !== '/') { 54 | $root .= '/'; 55 | } 56 | 57 | $moduleIterator = new DirectoryIterator($root); 58 | foreach ($moduleIterator as $module) { 59 | if ($module->isDot() || !$module->isDir()) { 60 | continue; 61 | } 62 | 63 | $moduleName = $module->getFilename(); 64 | 65 | $symbolFile = $moduleName; 66 | if (strrpos($symbolFile, '.pdb') !== false) { 67 | $symbolFile = substr($symbolFile, 0, -4); 68 | } 69 | $symbolFile .= '.sym.gz'; 70 | 71 | print($moduleName . ' (' . $symbolFile . ')' . PHP_EOL); 72 | 73 | $symbols = []; 74 | 75 | $symbolIterator = new DirectoryIterator($root . $moduleName); 76 | foreach ($symbolIterator as $symbol) { 77 | if ($symbol->isDot() || !$symbol->isDir()) { 78 | continue; 79 | } 80 | 81 | $symbolName = $symbol->getFilename(); 82 | 83 | $symbolFilePath = $root . $moduleName . '/' . $symbolName . '/' . $symbolFile; 84 | $symbolFileInfo = new SplFileInfo($symbolFilePath); 85 | $symbolFileTime = $symbolFileInfo->getMTime(); 86 | 87 | $symbols[$symbolFileTime] = $symbolName; 88 | 89 | //print('> ' . $symbolName . ' (' . $symbolFileTime . ')' . PHP_EOL); 90 | } 91 | 92 | ksort($symbols); 93 | 94 | $count = count($symbols); 95 | 96 | $symbols = array_slice($symbols, 0, -10, true); 97 | 98 | $cutoff = time() - (60 * 60 * 24 * 31 * 6); // Roughly 6 months. 99 | 100 | $deleted = 0; 101 | foreach ($symbols as $mtime => $symbolName) { 102 | if ($mtime > $cutoff) { 103 | break; 104 | } 105 | 106 | if (isset($usedMap[$moduleName]) && isset($usedMap[$moduleName][$symbolName])) { 107 | continue; 108 | } 109 | 110 | $symbolFileDir = $root . $moduleName . '/' . $symbolName; 111 | $symbolFilePath = $symbolFileDir . '/' . $symbolFile; 112 | 113 | //unlink($symbolFilePath); 114 | //rmdir($symbolFileDir); 115 | 116 | $deleted += 1; 117 | } 118 | 119 | print('... Deleted ' . $deleted . ' symbols (' . ($count - $deleted) . ' remaining)' . PHP_EOL); 120 | 121 | // TODO: Only run the first module for testing. 122 | //break; 123 | } 124 | 125 | -------------------------------------------------------------------------------- /scripts/crashstat.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | $identifiers) { 27 | foreach ($identifiers as $identifier => $stores) { 28 | if (count($stores) <= 1) { 29 | unset($symbols[$name][$identifier]); 30 | } 31 | } 32 | 33 | if (count($symbols[$name]) <= 0) { 34 | unset($symbols[$name]); 35 | } 36 | } 37 | 38 | //print_r($symbols); 39 | 40 | foreach ($symbols as $name => $identifiers) { 41 | foreach ($identifiers as $identifier => $stores) { 42 | sort($stores); 43 | if (count($stores) !== 2) { 44 | throw new Exception('not implemented'); 45 | } 46 | 47 | $file = $name; 48 | if (strrpos($file, '.pdb') !== false) { 49 | $file = substr($file, 0, -4); 50 | } 51 | $file .= '.sym'; 52 | 53 | $good = null; 54 | $bad = null; 55 | if ($stores[0] === 'public') { 56 | // Any store wins over public 57 | $good = $stores[1]; 58 | $bad = $stores[0]; 59 | } else if ($stores[0] === 'microsoft' && $stores[1] === 'mozilla') { 60 | // Official MS symbols win over Moz's 61 | $good = $stores[0]; 62 | $bad = $stores[1]; 63 | } else { 64 | throw new Exception('unhandled store pairing'); 65 | } 66 | 67 | print("Taking symbols for $name/$identifier from $good, removing $bad\n"); 68 | 69 | if (!file_exists("symbols/$bad/$name/$identifier")) { 70 | print(" already removed?\n"); 71 | continue; 72 | } 73 | 74 | if (file_exists("symbols/$bad/$name/$identifier/$file.gz")) { 75 | unlink("symbols/$bad/$name/$identifier/$file.gz"); 76 | } 77 | 78 | rmdir("symbols/$bad/$name/$identifier"); 79 | 80 | if (file_exists("cache/symbols/$name/$identifier")) { 81 | print(" also removing from cache\n"); 82 | 83 | if (file_exists("cache/symbols/$name/$identifier/$file")) { 84 | unlink("cache/symbols/$name/$identifier/$file"); 85 | } 86 | 87 | rmdir("cache/symbols/$name/$identifier"); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /scripts/extract_host_error.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | /dev/null', $_, $ret); 6 | if ($ret !== 0) { 7 | exit($ret); 8 | } 9 | 10 | $output = json_decode($output, true); 11 | if ($output === false) { 12 | exit(1); 13 | } 14 | 15 | //print_r($output); 16 | 17 | $rq = isset($output['requesting_thread']) ? $output['requesting_thread'] : 0; 18 | $thread = isset($output['threads']) ? (isset($output['threads'][$rq]) ? $output['threads'][$rq] : null) : null; 19 | 20 | if ($thread === null) { 21 | exit(1); 22 | } 23 | 24 | $stack = array_reduce(array_slice($thread, 0, 10), function($s, $t) { 25 | if (!isset($t['stack'])) return $s; 26 | return $s . base64_decode($t['stack']); 27 | }, ''); 28 | 29 | $ret = preg_match('/Host_Error: ([^\\x00]*)[\\x00]/', $stack, $matches); 30 | 31 | if ($ret === false) { 32 | exit(1); 33 | } 34 | 35 | if ($ret === 0) { 36 | print($argv[1].': [no error]'.PHP_EOL); 37 | exit(0); 38 | } 39 | 40 | $error = trim($matches[1]); 41 | print($argv[1].': '.$error.PHP_EOL); 42 | exit(0); 43 | 44 | -------------------------------------------------------------------------------- /scripts/get_all_maps.php: -------------------------------------------------------------------------------- 1 | writeErr('Loading minidumps...'); 10 | 11 | $query = $app['db']->executeQuery('SELECT id, metadata FROM crash'); 12 | 13 | $out->writeErr(' done.' . PHP_EOL); 14 | 15 | $maps = array(); 16 | 17 | $bar = id(new PhutilConsoleProgressBar())->setTotal($query->rowCount()); 18 | 19 | while ($row = $query->fetch()) { 20 | $bar->update(1); 21 | 22 | $metadata = json_decode($row['metadata'], true); 23 | 24 | if (!$metadata || empty($metadata['ExtensionVersion']) || version_compare($metadata['ExtensionVersion'], '2.2.1', '<')) { 25 | continue; 26 | } 27 | 28 | $id = $row['id']; 29 | $path = $app['root'] . '/dumps/' . substr($id, 0, 2) . '/' . $id . '.meta.txt.gz'; 30 | 31 | try { 32 | $metadata = gzdecode(\Filesystem::readFile($path)); 33 | } catch(Exception $e) { 34 | continue; 35 | } 36 | 37 | $ret = preg_match('/(?<=-------- CONFIG BEGIN --------)[^\\x00]+(?=-------- CONFIG END --------)/i', $metadata, $metadata); 38 | 39 | if ($ret !== 1) { 40 | continue; 41 | } 42 | 43 | $metadata = phutil_split_lines(trim($metadata[0]), false); 44 | 45 | foreach ($metadata as $line) { 46 | list($key, $value) = explode('=', $line, 2); 47 | 48 | if ($key !== 'Map') { 49 | continue; 50 | } 51 | 52 | if (!array_key_exists($value, $maps)) { 53 | $maps[$value] = 1; 54 | } else { 55 | $maps[$value] += 1; 56 | } 57 | 58 | break; 59 | } 60 | } 61 | 62 | $bar->done(); 63 | 64 | arsort($maps); 65 | 66 | print_r($maps); 67 | 68 | -------------------------------------------------------------------------------- /scripts/os_versions.php: -------------------------------------------------------------------------------- 1 | writeErr('Loading minidumps...'); 16 | 17 | $query = $app['db']->executeQuery('SELECT id FROM crash WHERE processed = 1 AND failed = 0'); 18 | 19 | $out->writeErr(' done. (' . $query->rowCount() . ')' . PHP_EOL); 20 | 21 | $bar = id(new PhutilConsoleProgressBar())->setTotal($query->rowCount()); 22 | 23 | $versions = array(); 24 | 25 | while ($row = $query->fetch()) { 26 | $bar->update(1); 27 | 28 | try { 29 | $id = $row['id']; 30 | $path = $app['root'] . '/dumps/' . substr($id, 0, 2) . '/' . $id . '.dmp'; 31 | $minidump = \Filesystem::readFile($path); 32 | 33 | $output = array('id' => $id); 34 | 35 | $output['header'] = $header = unpack('A4magic/Lversion/Lstream_count/Lstream_offset', $minidump); 36 | 37 | $stream_offset = $header['stream_offset']; 38 | $stream = false; 39 | do { 40 | $output['stream'] = $stream = unpack('Ltype/Lsize/Loffset', $minidump, $stream_offset); 41 | $stream_offset += 12; 42 | } while ($stream !== false && $stream['type'] !== 7); 43 | 44 | if ($stream === false) { 45 | throw new \RuntimeException('Missing MD_SYSTEM_INFO_STREAM'); 46 | } 47 | 48 | $output['system'] = $system = unpack('vprocessor_architecture/vprocessor_level/vprocessor_revision/Cnumber_of_processors/Cproduct_type/Vmajor_version/Vminor_version/Vbuild_number/Vplatform_id/Vcsd_version_rva/vsuite_mask/vreserved2', $minidump, $stream['offset']); 49 | $output['version_length'] = $version_length = unpack('V', $minidump, $system['csd_version_rva'])[1]; 50 | $output['version_raw'] = $version_raw = unpack('a'.$version_length, $minidump, $system['csd_version_rva'] + 4)[1]; 51 | $output['version'] = $version = iconv('utf-16', 'utf-8', $version_raw); 52 | 53 | $string = ''; 54 | 55 | // if ($system['platform_id'] >= 0x1000) continue; 56 | // if ($system['major_version'] <= 5) { print(PHP_EOL); var_dump($id); } 57 | 58 | switch ($system['platform_id']) { 59 | case 2: 60 | $string .= 'Windows NT'; 61 | break; 62 | case 0x8101: 63 | $string .= 'macOS'; 64 | break; 65 | case 0x8201: 66 | $string .= 'Linux'; 67 | break; 68 | default: 69 | $string .= sprintf('[Platform 0x%X]', $system['platform_id']); 70 | } 71 | 72 | $string .= sprintf(' %d.%d.%d', $system['major_version'], $system['minor_version'], $system['build_number']); 73 | 74 | if (strlen($version)) { 75 | $string .= ' '.$version; 76 | } 77 | 78 | if (isset($versions[$string])) { 79 | $versions[$string]++; 80 | } else { 81 | $versions[$string] = 1; 82 | } 83 | } catch (Throwable $e) { 84 | // print(PHP_EOL); var_dump($e->getMessage()); 85 | continue; 86 | } 87 | } 88 | 89 | $bar->done(); 90 | 91 | arsort($versions); 92 | print_r($versions); 93 | -------------------------------------------------------------------------------- /src/Throttle/Command/CrashCleanCommand.php: -------------------------------------------------------------------------------- 1 | setName('crash:clean') 16 | ->setDescription('Cleanup old crash dumps.') 17 | ->addOption( 18 | 'dry-run', 19 | null, 20 | InputOption::VALUE_NONE, 21 | 'Only list orphan dumps, do not delete them.' 22 | ); 23 | } 24 | 25 | protected function execute(InputInterface $input, OutputInterface $output) 26 | { 27 | $app = $this->getApplication()->getContainer(); 28 | 29 | $total_count = 0; 30 | 31 | $groups = $app['db']->executeQuery('SELECT owner, ip, INET6_NTOA(ip) AS display_ip FROM crash WHERE lastview IS NULL OR lastview < DATE_SUB(NOW(), INTERVAL 90 DAY) GROUP BY owner, ip HAVING (owner IS NULL AND COUNT(*) > 50) OR (owner IS NOT NULL AND COUNT(*) > 100)'); 32 | 33 | while ($group = $groups->fetch()) { 34 | if ($group['owner'] !== null) { 35 | $query = $app['db']->executeQuery('SELECT id FROM crash WHERE owner = ? AND ip = ? AND (lastview IS NULL OR lastview < DATE_SUB(NOW(), INTERVAL 90 DAY)) ORDER BY timestamp DESC LIMIT 100 OFFSET 100', array($group['owner'], $group['ip'])); 36 | } else { 37 | $query = $app['db']->executeQuery('SELECT id FROM crash WHERE owner IS NULL AND ip = ? AND (lastview IS NULL OR lastview < DATE_SUB(NOW(), INTERVAL 90 DAY)) ORDER BY timestamp DESC LIMIT 100 OFFSET 50', array($group['ip'])); 38 | } 39 | 40 | $crashes = array(); 41 | while ($id = $query->fetchColumn(0)) { 42 | $crashes[] = $id; 43 | } 44 | 45 | $count = count($crashes); 46 | if (!$input->getOption('dry-run') && $count > 0) { 47 | $count = $app['db']->executeUpdate('DELETE FROM crash WHERE id IN (?) LIMIT 100', array($crashes), array(\Doctrine\DBAL\Connection::PARAM_STR_ARRAY)); 48 | 49 | if ($count > 0) { 50 | $app['redis']->hIncrBy('throttle:stats', 'crashes:cleaned:limit', $count); 51 | } 52 | } 53 | 54 | $output->writeln('Deleted ' . $count . ' crash dumps for server: ' . ($group['owner'] ?: 'NULL') . ', ' . $group['display_ip']); 55 | $total_count += $count; 56 | } 57 | 58 | $count = 0; 59 | if ($input->getOption('dry-run')) { 60 | $count = $app['db']->executeQuery('SELECT LEAST(COUNT(*), 100) FROM crash WHERE COALESCE(lastview, timestamp) < DATE_SUB(NOW(), INTERVAL 90 DAY)')->fetchColumn(0); 61 | } else { 62 | // Using COALESCE for this query locks every single row for writing. 63 | // $count = $app['db']->executeUpdate('DELETE FROM crash WHERE COALESCE(lastview, timestamp) < DATE_SUB(NOW(), INTERVAL 90 DAY) LIMIT 100'); 64 | $count = $app['db']->executeUpdate('DELETE FROM crash WHERE (lastview IS NOT NULL AND lastview < DATE_SUB(NOW(), INTERVAL 90 DAY)) OR (lastview IS NULL AND timestamp < DATE_SUB(NOW(), INTERVAL 90 DAY)) LIMIT 100'); 65 | 66 | if ($count > 0) { 67 | $app['redis']->hIncrBy('throttle:stats', 'crashes:cleaned:old', $count); 68 | } 69 | } 70 | 71 | $output->writeln('Deleted ' . $count . ' old crash dumps.'); 72 | $total_count += $count; 73 | 74 | $output->writeln('Removed ' . $total_count . ' crash dumps from database.'); 75 | 76 | $query = $app['db']->executeQuery('SELECT id FROM crash'); 77 | 78 | $crashes = array(); 79 | while ($id = $query->fetchColumn(0)) { 80 | $crashes[$id] = true; 81 | } 82 | 83 | $output->writeln('Got ' . count($crashes) . ' crashes in database.'); 84 | 85 | $count = 0; 86 | $found = array(); 87 | $buckets = \Filesystem::listDirectory($app['root'] . '/dumps', false); 88 | foreach ($buckets as $bucket) { 89 | $dumps = \Filesystem::listDirectory($app['root'] . '/dumps/' . $bucket, false); 90 | foreach ($dumps as $dump) { 91 | $filename = explode('.', $dump, 2); 92 | 93 | if (isset($crashes[$filename[0]])) { 94 | if ($filename[1] === 'dmp') { 95 | $found[$filename[0]] = true; 96 | } 97 | 98 | continue; 99 | } 100 | 101 | $path = $app['root'] . '/dumps/' . $bucket . '/' . $dump; 102 | 103 | if (time() - filemtime($path) < 24 * 3600) { 104 | // Wait until the file is 24hrs old before removing it, just in case we're racing submission. 105 | continue; 106 | } 107 | 108 | if (!$input->getOption('dry-run')) { 109 | \Filesystem::remove($path); 110 | } 111 | 112 | if ($filename[1] === 'dmp') { 113 | $count++; 114 | } 115 | } 116 | } 117 | 118 | $output->writeln('Matched ' . count($found) . ' minidumps to database.'); 119 | 120 | if ($count > 0 && !$input->getOption('dry-run')) { 121 | $app['redis']->hIncrBy('throttle:stats', 'crashes:cleaned:orphan', $count); 122 | } 123 | 124 | $output->writeln('Deleted ' . $count . ' orphaned minidumps.'); 125 | 126 | $missing = array_diff_key($crashes, $found); 127 | 128 | if (count($missing) > 0) { 129 | $query = $app['db']->executeQuery('SELECT id FROM crash WHERE id IN (?) AND failed = 1', array(array_keys($missing)), array(\Doctrine\DBAL\Connection::PARAM_STR_ARRAY)); 130 | 131 | $missing_errored = array(); 132 | while ($id = $query->fetchColumn(0)) { 133 | $missing_errored[$id] = true; 134 | } 135 | 136 | $count = count($missing_errored); 137 | if (!$input->getOption('dry-run')) { 138 | $count = $app['db']->executeUpdate('DELETE FROM crash WHERE id IN (?) AND failed = 1 AND timestamp < DATE_SUB(NOW(), INTERVAL 1 DAY) LIMIT 100', array(array_keys($missing)), array(\Doctrine\DBAL\Connection::PARAM_STR_ARRAY)); 139 | 140 | if ($count > 0) { 141 | $app['redis']->hIncrBy('throttle:stats', 'crashes:cleaned:missing', $count); 142 | } 143 | } 144 | 145 | $output->writeln('Deleted ' . $count . ' failed crash reports missing minidumps.'); 146 | 147 | $anomalous = array_diff_key($missing, $missing_errored); 148 | 149 | if (count($anomalous) > 0) { 150 | $output->writeln('Found ' . count($anomalous) . ' crash reports missing minidumps that need investigating:'); 151 | 152 | print_r($anomalous); 153 | } 154 | } 155 | } 156 | } 157 | 158 | -------------------------------------------------------------------------------- /src/Throttle/Command/CrashStatsCommand.php: -------------------------------------------------------------------------------- 1 | setName('crash:stats') 16 | ->setDescription('Display statistics about crashes.') 17 | ->addOption( 18 | 'limit', 19 | 'l', 20 | InputOption::VALUE_REQUIRED, 21 | 'Number of crashes to display', 22 | 20 23 | ); 24 | } 25 | 26 | protected function execute(InputInterface $input, OutputInterface $output) 27 | { 28 | $app = $this->getApplication()->getContainer(); 29 | 30 | $table = $this->getHelperSet()->get('table'); 31 | $table->setCellHeaderFormat('%s'); 32 | $table->setCellRowFormat('%s'); 33 | 34 | $table->setHeaders(array('', 'Function')); 35 | 36 | $query = $app['db']->executeQuery('SELECT COUNT(frame.rendered) as count, frame.rendered FROM crash JOIN frame ON frame.crash = crash.id AND frame.thread = crash.thread AND frame.frame = 0 WHERE timestamp > DATE_SUB(NOW(), INTERVAL 1 MONTH) GROUP BY frame.rendered ORDER BY count DESC LIMIT ?', array($input->getOption('limit')), array(\PDO::PARAM_INT)); 37 | while (($module = $query->fetch()) !== false) { 38 | $table->addRow($module); 39 | } 40 | 41 | $table->render($output); 42 | } 43 | } 44 | 45 | -------------------------------------------------------------------------------- /src/Throttle/Command/SymbolsDownloadCommand.php: -------------------------------------------------------------------------------- 1 | setName('symbols:download') 16 | ->setDescription('Download missing symbol files from the Microsoft Symbol Server.') 17 | ->addArgument( 18 | 'name', 19 | InputArgument::OPTIONAL, 20 | 'Module Name' 21 | ) 22 | ->addArgument( 23 | 'identifier', 24 | InputArgument::OPTIONAL, 25 | 'Module Identifier' 26 | ) 27 | ->addOption( 28 | 'limit', 29 | 'l', 30 | InputOption::VALUE_REQUIRED, 31 | 'Limit' 32 | ); 33 | } 34 | 35 | protected function execute(InputInterface $input, OutputInterface $output) 36 | { 37 | $app = $this->getApplication()->getContainer(); 38 | 39 | if (\Filesystem::resolveBinary('wine') === null || \Filesystem::resolveBinary('cabextract') === null) { 40 | throw new \RuntimeException('\'wine\' and \'cabextract\' need to be available in your PATH to use this command'); 41 | } 42 | 43 | $limit = $input->getOption('limit'); 44 | 45 | if ($limit !== null && !ctype_digit($limit)) { 46 | throw new \InvalidArgumentException('\'limit\' must be an integer'); 47 | } 48 | 49 | // Initialize the wine environment. 50 | execx('WINEPREFIX=%s WINEDEBUG=-all wine regsvr32 %s', $app['root'] . '/.wine', $app['root'] . '/bin/msdia80.dll'); 51 | 52 | $manualName = $input->getArgument('name'); 53 | $manualIdentifier = $input->getArgument('identifier'); 54 | 55 | $modules = Array(); 56 | 57 | if ($manualName) { 58 | if (!$manualIdentifier) { 59 | throw new \RuntimeException('Specifying \'name\' requires specifying \'identifier\' as well.'); 60 | } 61 | 62 | $modules[] = Array('name' => $manualName, 'identifier' => $manualIdentifier); 63 | } else { 64 | // Find all Windows modules missing symbols 65 | $query = 'SELECT DISTINCT name, identifier FROM module WHERE name LIKE \'%.pdb\' AND present = 0'; 66 | 67 | $modules = $app['db']->executeQuery($query)->fetchAll(); 68 | } 69 | 70 | $blacklist = null; 71 | 72 | $blacklistJson = $app['redis']->get('throttle:cache:blacklist'); 73 | 74 | if ($blacklistJson) { 75 | $blacklist = json_decode($blacklistJson, true); 76 | } 77 | 78 | if (!$blacklist) { 79 | $blacklist = array(); 80 | } 81 | 82 | $output->writeln('Loaded ' . count($blacklist) . ' blacklist entries'); 83 | 84 | foreach ($modules as $key => $module) { 85 | $name = $module['name']; 86 | $identifier = $module['identifier']; 87 | 88 | if (!$manualName && isset($blacklist[$name])) { 89 | if ($blacklist[$name]['_total'] >= 9) { 90 | unset($modules[$key]); 91 | continue; 92 | } 93 | 94 | if (isset($blacklist[$name][$identifier])) { 95 | if ($blacklist[$name][$identifier] >= 3) { 96 | unset($modules[$key]); 97 | continue; 98 | } 99 | } 100 | } 101 | } 102 | 103 | shuffle($modules); 104 | 105 | if ($limit) { 106 | $modules = array_slice($modules, 0, $limit); 107 | } 108 | 109 | $count = count($modules); 110 | $output->writeln('Found ' . $count . ' missing symbols'); 111 | 112 | // Prepare HTTPSFutures for downloading PDBs. 113 | $futures = array(); 114 | foreach ($modules as $key => $module) { 115 | $name = $module['name']; 116 | $identifier = $module['identifier']; 117 | 118 | $futures[$key] = id(new \HTTPSFuture('http://msdl.microsoft.com/download/symbols/' . urlencode($name) . '/' . $identifier . '/' . urlencode($name))) 119 | ->addHeader('User-Agent', 'Microsoft-Symbol-Server')->setExpectStatus(array(200, 404)); 120 | } 121 | 122 | $downloaded = 0; 123 | $count = count($futures); 124 | $output->writeln('Downloading ' . $count . ' missing symbols'); 125 | 126 | if ($count === 0) { 127 | return; 128 | } 129 | 130 | $cache = \Filesystem::createDirectory($app['root'] . '/cache/pdbs', 0777, true); 131 | 132 | $progress = $this->getHelperSet()->get('progress'); 133 | $progress->start($output, $count); 134 | 135 | // Only run 10 concurrent requests. 136 | // I'm unsure on what MS would consider fair here, 1 might be better but is slooooow. 137 | // FutureIterator returns them in the order they resolve, so running concurrently lets the later stages optimize. 138 | foreach (id(new \FutureIterator($futures))->limit(10) as $key => $future) { 139 | list($status, $body, $headers) = $future->resolve(); 140 | 141 | if ($status->isError()) { 142 | throw $status; 143 | } 144 | 145 | $module = $modules[$key]; 146 | 147 | $name = $module['name']; 148 | $identifier = $module['identifier']; 149 | 150 | if ($status instanceof \HTTPFutureHTTPResponseStatus && $status->getStatusCode() === 404) { 151 | if (!isset($blacklist[$name])) { 152 | $blacklist[$name] = [ 153 | '_total' => 1, 154 | $identifier => 1, 155 | ]; 156 | } else { 157 | $blacklist[$name]['_total'] += 1; 158 | 159 | if (!isset($blacklist[$name][$identifier])) { 160 | $blacklist[$name][$identifier] = 1; 161 | } else { 162 | $blacklist[$name][$identifier] += 1; 163 | } 164 | } 165 | 166 | if ($manualName) { 167 | $output->writeln("\r" . 'Failed to download: ' . $name . ' ' . $identifier); 168 | } 169 | 170 | $app['redis']->set('throttle:cache:blacklist', json_encode($blacklist)); 171 | 172 | $progress->advance(); 173 | continue; 174 | } 175 | 176 | // Reset the total on any successful download. 177 | if (isset($blacklist[$name])) { 178 | $blacklist[$name]['_total'] = 0; 179 | if (isset($blacklist[$name][$identifier])) { 180 | $blacklist[$name][$identifier] = 0; 181 | } 182 | } 183 | 184 | $app['redis']->set('throttle:cache:blacklist', json_encode($blacklist)); 185 | 186 | $prefix = $cache . '/' . $name . '-' . $identifier; 187 | 188 | // Write the PDB. 189 | \Filesystem::createDirectory($prefix, 0777, true); 190 | \Filesystem::writeFile($prefix . '/' . $name, $body); 191 | 192 | // Finally, dump the symbols. 193 | $symfile = substr($name, 0, -3) . 'sym.gz'; 194 | $symdir = \Filesystem::createDirectory($app['root'] . '/symbols/microsoft/' . $name . '/' . $identifier, 0755, true); 195 | 196 | $failed = false; 197 | try { 198 | execx('WINEPREFIX=%s WINEDEBUG=-all wine %s %s | gzip > %s', 199 | $app['root'] . '/.wine', $app['root'] . '/bin/dump_syms.exe', $prefix . '/' . $name, $symdir . '/' . $symfile); 200 | 201 | $downloaded += 1; 202 | } catch (\CommandException $e) { 203 | $failed = true; 204 | $output->writeln("\r" . 'Failed to process: ' . $name . ' ' . $identifier); 205 | 206 | // While a bit messy, we need to delete the orphan symbol file to stop it being marked as present. 207 | \Filesystem::remove($symdir . '/' . $symfile); 208 | } 209 | 210 | // Delete the PDB and the working dir. 211 | \Filesystem::remove($prefix . '/' . $name); 212 | \Filesystem::remove($prefix); 213 | 214 | // And finally mark the module as having symbols present. 215 | $app['db']->executeUpdate('UPDATE module SET present = ? WHERE name = ? AND identifier = ?', array(!$failed, $name, $identifier)); 216 | 217 | $progress->advance(); 218 | } 219 | 220 | $app['redis']->set('throttle:cache:blacklist', json_encode($blacklist)); 221 | 222 | $progress->finish(); 223 | 224 | \Filesystem::remove($cache); 225 | 226 | if ($downloaded <= 0) { 227 | return; 228 | } 229 | 230 | $output->writeln('Waiting for processing lock...'); 231 | 232 | $lock = \PhutilFileLock::newForPath($app['root'] . '/cache/process.lck'); 233 | $lock->lock(300); 234 | 235 | $app['redis']->del('throttle:cache:symbol'); 236 | 237 | $output->writeln('Flushed symbol cache'); 238 | 239 | $lock->unlock(); 240 | } 241 | } 242 | 243 | -------------------------------------------------------------------------------- /src/Throttle/Command/SymbolsDumpCommand.php: -------------------------------------------------------------------------------- 1 | setName('symbols:dump') 16 | ->setDescription('Dump symbol data from binary. This should only be used for binaries missing debugging information.') 17 | ->addArgument( 18 | 'binary', 19 | InputArgument::IS_ARRAY | InputArgument::REQUIRED, 20 | 'Binaries to process, seperate multiple values with a space' 21 | ); 22 | } 23 | 24 | protected function execute(InputInterface $input, OutputInterface $output) 25 | { 26 | $app = $this->getApplication()->getContainer(); 27 | 28 | $table = $this->getHelperSet()->get('table'); 29 | $table->setCellHeaderFormat('%s'); 30 | $table->setCellRowFormat('%s'); 31 | $table->setHeaders(array('Binary', 'Identifier')); 32 | 33 | $moduleFutures = array(); 34 | $symbolFutures = array(); 35 | $binaries = $input->getArgument('binary'); 36 | foreach ($binaries as $binary) { 37 | $moduleFutures[$binary] = new \ExecFuture($app['root'] . '/bin/breakpad_moduleid %s', $binary); 38 | $symbolFutures[$binary] = new \ExecFuture($app['root'] . '/bin/nm -nC %s', $binary); 39 | } 40 | 41 | $identifiers = array(); 42 | foreach (id(new \FutureIterator($moduleFutures))->limit(5) as $name => $future) { 43 | list($stdout, $stderr) = $future->resolvex(); 44 | $identifier = rtrim($stdout); 45 | 46 | $identifiers[$name] = $identifier; 47 | $table->addRow(array(basename($name), $identifier)); 48 | } 49 | 50 | foreach (id(new \FutureIterator($symbolFutures))->limit(5) as $name => $future) { 51 | $basename = basename($name); 52 | $identifier = $identifiers[$name]; 53 | 54 | $path = $app['root'] . '/symbols/public/' . $basename . '/' . $identifier; 55 | $file = $path . '/' . $basename . '.sym'; 56 | \Filesystem::createDirectory($path, 0777, true); 57 | 58 | \Filesystem::writeFile($file, 'MODULE Linux x86 ' . $identifier . ' ' . $basename . PHP_EOL); 59 | foreach (new \LinesOfALargeExecFuture($future) as $line) { 60 | if (!preg_match('/^0+([0-9a-fA-F]+) +[tT] +([0-9a-zA-Z_.* ,():&]+)$/', $line, $matches)) { 61 | continue; 62 | } 63 | 64 | \Filesystem::appendFile($file, 'PUBLIC ' . $matches[1] . ' 0 ' . $matches[2] . PHP_EOL); 65 | } 66 | } 67 | 68 | $table->render($output); 69 | } 70 | } 71 | 72 | -------------------------------------------------------------------------------- /src/Throttle/Command/SymbolsMozillaDownloadCommand.php: -------------------------------------------------------------------------------- 1 | setName('symbols:mozilla:download') 16 | ->setDescription('Download missing symbol files from the Mozilla Symbol Server.') 17 | ->addArgument( 18 | 'name', 19 | InputArgument::OPTIONAL, 20 | 'Module Name' 21 | ) 22 | ->addArgument( 23 | 'identifier', 24 | InputArgument::OPTIONAL, 25 | 'Module Identifier' 26 | ) 27 | ->addOption( 28 | 'limit', 29 | 'l', 30 | InputOption::VALUE_REQUIRED, 31 | 'Limit' 32 | ); 33 | } 34 | 35 | protected function execute(InputInterface $input, OutputInterface $output) 36 | { 37 | $app = $this->getApplication()->getContainer(); 38 | 39 | $limit = $input->getOption('limit'); 40 | 41 | if ($limit !== null && !ctype_digit($limit)) { 42 | throw new \InvalidArgumentException('\'limit\' must be an integer'); 43 | } 44 | 45 | $manualName = $input->getArgument('name'); 46 | $manualIdentifier = $input->getArgument('identifier'); 47 | 48 | $modules = Array(); 49 | 50 | if ($manualName) { 51 | if (!$manualIdentifier) { 52 | throw new \RuntimeException('Specifying \'name\' requires specifying \'identifier\' as well.'); 53 | } 54 | 55 | $modules[] = Array('name' => $manualName, 'identifier' => $manualIdentifier); 56 | } else { 57 | $query = 'SELECT DISTINCT name, identifier FROM module WHERE present = 0'; 58 | 59 | if ($limit !== null) { 60 | $query .= ' LIMIT ' . $limit; 61 | } 62 | 63 | $modules = $app['db']->executeQuery($query)->fetchAll(); 64 | } 65 | 66 | $blacklist = null; 67 | 68 | $blacklistJson = $app['redis']->get('throttle:cache:blacklist:mozilla'); 69 | 70 | if ($blacklistJson) { 71 | $blacklist = json_decode($blacklistJson, true); 72 | } 73 | 74 | if (!$blacklist) { 75 | $blacklist = array(); 76 | } 77 | 78 | $output->writeln('Loaded ' . count($blacklist) . ' blacklist entries'); 79 | 80 | $count = count($modules); 81 | $output->writeln('Found ' . $count . ' missing symbols'); 82 | 83 | // Prepare HTTPSFutures for downloading compressed PDBs. 84 | $futures = array(); 85 | foreach ($modules as $key => $module) { 86 | $name = $module['name']; 87 | $identifier = $module['identifier']; 88 | 89 | if (isset($blacklist[$name])) { 90 | if ($blacklist[$name]['_total'] >= 9) { 91 | continue; 92 | } 93 | 94 | if (isset($blacklist[$name][$identifier])) { 95 | if ($blacklist[$name][$identifier] >= 3) { 96 | continue; 97 | } 98 | } 99 | } 100 | 101 | $symname = $name; 102 | if (substr($symname, -4) === '.pdb') { 103 | $symname = substr($symname, 0, -4); 104 | } 105 | $symname .= '.sym'; 106 | 107 | $futures[$key] = id(new \HTTPSFuture('https://s3-us-west-2.amazonaws.com/org.mozilla.crash-stats.symbols-public/v1/' . urlencode($name) . '/' . $identifier . '/' . urlencode($symname))) 108 | ->setExpectStatus(array(200, 404)); 109 | } 110 | 111 | $count = count($futures); 112 | $output->writeln('Downloading ' . $count . ' missing symbols'); 113 | 114 | if ($count === 0) { 115 | return; 116 | } 117 | 118 | $progress = $this->getHelperSet()->get('progress'); 119 | $progress->start($output, $count); 120 | 121 | $failed = 0; 122 | 123 | // Only run 10 concurrent requests. 124 | // I'm unsure on what Moz would consider fair here, 1 might be better but is slooooow. 125 | // FutureIterator returns them in the order they resolve, so running concurrently lets the later stages optimize. 126 | foreach (id(new \FutureIterator($futures))->limit(10) as $key => $future) { 127 | list($status, $body, $headers) = $future->resolve(); 128 | 129 | if ($status->isError()) { 130 | throw $status; 131 | } 132 | 133 | $module = $modules[$key]; 134 | 135 | $name = $module['name']; 136 | $identifier = $module['identifier']; 137 | 138 | if ($status instanceof \HTTPFutureHTTPResponseStatus && $status->getStatusCode() === 404) { 139 | //$output->writeln(''); 140 | //$output->writeln(json_encode($module)); 141 | 142 | if (!isset($blacklist[$name])) { 143 | $blacklist[$name] = [ 144 | '_total' => 1, 145 | $identifier => 1, 146 | ]; 147 | } else { 148 | $blacklist[$name]['_total'] += 1; 149 | 150 | if (!isset($blacklist[$name][$identifier])) { 151 | $blacklist[$name][$identifier] = 1; 152 | } else { 153 | $blacklist[$name][$identifier] += 1; 154 | } 155 | } 156 | 157 | $failed += 1; 158 | 159 | if (($failed % 50) === 0) { 160 | $output->writeln(''); 161 | $output->writeln('Sending blacklist checkpoint...'); 162 | 163 | $app['redis']->set('throttle:cache:blacklist:mozilla', json_encode($blacklist)); 164 | } 165 | 166 | $progress->advance(); 167 | continue; 168 | } 169 | 170 | // Reset the total on any successful download. 171 | if (isset($blacklist[$name])) { 172 | $blacklist[$name]['_total'] = 0; 173 | } 174 | 175 | $symdir = \Filesystem::createDirectory($app['root'] . '/symbols/mozilla/' . $name . '/' . $identifier, 0755, true); 176 | 177 | $symname = $name; 178 | if (substr($symname, -4) === '.pdb') { 179 | $symname = substr($symname, 0, -4); 180 | } 181 | $symname .= '.sym.gz'; 182 | 183 | \Filesystem::writeFile($symdir . '/' . $symname, $body); 184 | 185 | // And finally mark the module as having symbols present. 186 | $app['db']->executeUpdate('UPDATE module SET present = ? WHERE name = ? AND identifier = ?', array(1, $name, $identifier)); 187 | 188 | $progress->advance(); 189 | } 190 | 191 | $app['redis']->set('throttle:cache:blacklist:mozilla', json_encode($blacklist)); 192 | 193 | $progress->finish(); 194 | 195 | $output->writeln($failed . ' symbols failed to download'); 196 | 197 | if ($failed === $count) { 198 | return; 199 | } 200 | 201 | $output->writeln('Waiting for processing lock...'); 202 | 203 | $lock = \PhutilFileLock::newForPath($app['root'] . '/cache/process.lck'); 204 | $lock->lock(300); 205 | 206 | $app['redis']->del('throttle:cache:symbol'); 207 | 208 | $output->writeln('Flushed symbol cache'); 209 | 210 | $lock->unlock(); 211 | } 212 | } 213 | 214 | -------------------------------------------------------------------------------- /src/Throttle/Command/SymbolsStatsCommand.php: -------------------------------------------------------------------------------- 1 | setName('symbols:stats') 23 | ->setDescription('Display statistics about symbol files.') 24 | ->addOption( 25 | 'unused', 26 | 'u', 27 | InputOption::VALUE_NONE, 28 | 'List unused symbol files' 29 | ) 30 | ->addOption( 31 | 'crashers', 32 | 'c', 33 | InputOption::VALUE_NONE, 34 | 'Only look at modules that caused crashes (Warning: This is slow)' 35 | ) 36 | ->addOption( 37 | 'limit', 38 | 'l', 39 | InputOption::VALUE_REQUIRED, 40 | 'Number of most-frequent symbol files to display', 41 | 10 42 | ); 43 | } 44 | 45 | protected function execute(InputInterface $input, OutputInterface $output) 46 | { 47 | $app = $this->getApplication()->getContainer(); 48 | 49 | $table = $this->getHelperSet()->get('table'); 50 | $table->setCellHeaderFormat('%s'); 51 | $table->setCellRowFormat('%s'); 52 | 53 | $moduleHeap = new ModuleMaxHeap; 54 | $topModules = array(); 55 | 56 | $databaseModules = array(); 57 | 58 | $query = null; 59 | if ($input->getOption('crashers')) { 60 | $query = $app['db']->executeQuery('SELECT COUNT(crash.id) as count, module.name, module.identifier FROM crash JOIN frame ON crash.id = frame.crash AND crash.thread = frame.thread AND frame.frame = 0 JOIN module ON crash.id = module.crash AND frame.module = module.name AND module.identifier != \'000000000000000000000000000000000\' GROUP BY module.name, module.identifier'); 61 | } else { 62 | $query = $app['db']->executeQuery('SELECT COUNT(crash) as count, name, identifier FROM module WHERE identifier != \'000000000000000000000000000000000\' GROUP BY name, identifier'); 63 | } 64 | 65 | while (($module = $query->fetch()) !== false) { 66 | if (!isset($databaseModules[$module['name']])) { 67 | $databaseModules[$module['name']] = array(); 68 | } 69 | 70 | $databaseModules[$module['name']][] = $module['identifier']; 71 | 72 | $moduleHeap->insert($module); 73 | } 74 | 75 | for ($i = 0; $i < $input->getOption('limit'); $i++) { 76 | $topModules[] = $moduleHeap->extract(); 77 | } 78 | unset($moduleHeap); 79 | 80 | $filesystemModules = array(); 81 | $stores = \Filesystem::listDirectory($app['root'] . '/symbols', false); 82 | foreach ($stores as $store) { 83 | $binaries = \Filesystem::listDirectory($app['root'] . '/symbols/' . $store, false); 84 | foreach ($binaries as $binary) { 85 | $identifiers = \Filesystem::listDirectory($app['root'] . '/symbols/' . $store . '/' . $binary, false); 86 | foreach ($identifiers as $identifier) { 87 | if (!isset($filesystemModules[$binary])) { 88 | $filesystemModules[$binary] = array(); 89 | } 90 | 91 | $filesystemModules[$binary][] = $identifier; 92 | } 93 | } 94 | } 95 | unset($stores); 96 | 97 | $total = 0; 98 | $matched = 0; 99 | foreach ($databaseModules as $name => $identifiers) { 100 | if (!isset($filesystemModules[$name])) { 101 | $total += count($identifiers); 102 | continue; 103 | } 104 | 105 | $otherIdentifiers = $filesystemModules[$name]; 106 | foreach ($identifiers as $identifier) { 107 | $total += 1; 108 | 109 | foreach ($otherIdentifiers as $otherIdentifier) { 110 | if ($otherIdentifier == $identifier) { 111 | $matched += 1; 112 | break; 113 | } 114 | } 115 | } 116 | } 117 | $percent = ($matched / $total) * 100; 118 | $output->writeln('Symbol file coverage: ' . $percent . '%'); 119 | 120 | if ($input->getOption('unused')) { 121 | $total = 0; 122 | $matched = 0; 123 | $unusedSymbols = array(); 124 | foreach ($filesystemModules as $name => $identifiers) { 125 | if (!isset($databaseModules[$name])) { 126 | $unusedSymbols[] = $name; 127 | $total += count($identifiers); 128 | continue; 129 | } 130 | 131 | $otherIdentifiers = $databaseModules[$name]; 132 | foreach ($identifiers as $identifier) { 133 | $total += 1; 134 | 135 | foreach ($otherIdentifiers as $otherIdentifier) { 136 | if ($otherIdentifier == $identifier) { 137 | $matched += 1; 138 | break; 139 | } 140 | } 141 | } 142 | } 143 | $percent = ($matched / $total) * 100; 144 | $output->writeln('Unused symbol files: ' . (100 - $percent) . '%'); 145 | 146 | $output->writeln(''); 147 | 148 | $output->writeln('Completely unused symbol files:'); 149 | $table->setHeaders(array()); 150 | $stride = 0; 151 | $row = array(); 152 | $table->setRows(array()); 153 | foreach ($unusedSymbols as $name) { 154 | if ($stride == 4) { 155 | $table->addRow($row); 156 | $row = array(); 157 | $stride = 0; 158 | } 159 | 160 | $row[] = $name; 161 | $stride += 1; 162 | } 163 | $table->addRow($row); 164 | $table->render($output); 165 | } 166 | 167 | if ($input->getOption('limit') > 0) { 168 | $output->writeln(''); 169 | 170 | $output->writeln('Most frequent modules:'); 171 | $table->setHeaders(array('Crashes', 'Binary', 'Identifier', 'Symbols')); 172 | $table->setRows(array()); 173 | foreach ($topModules as $module) { 174 | $found = false; 175 | 176 | if (isset($filesystemModules[$module['name']])) { 177 | $identifiers = $filesystemModules[$module['name']]; 178 | foreach ($identifiers as $identifier) { 179 | if ($identifier == $module['identifier']) { 180 | $found = true; 181 | break; 182 | } 183 | } 184 | } 185 | 186 | $table->addRow(array($module['count'], $module['name'], $module['identifier'], ($found ? 'YES' : 'NO'))); 187 | } 188 | $table->render($output); 189 | } 190 | } 191 | } 192 | 193 | -------------------------------------------------------------------------------- /src/Throttle/Command/SymbolsUpdateCommand.php: -------------------------------------------------------------------------------- 1 | setName('symbols:update') 16 | ->setDescription('Update module information in database.') 17 | ->addOption( 18 | 'clean', 19 | 'c', 20 | InputOption::VALUE_NONE, 21 | 'Rebuild all module information rather than just missing' 22 | ); 23 | } 24 | 25 | protected function execute(InputInterface $input, OutputInterface $output) 26 | { 27 | $app = $this->getApplication()->getContainer(); 28 | 29 | $symbols = \Filesystem::listDirectory($app['root'] . '/symbols'); 30 | $query = $app['db']->executeQuery('SELECT DISTINCT name, identifier FROM module WHERE identifier != \'000000000000000000000000000000000\'' . ($input->getOption('clean') ? '' : ' AND present = 0')); 31 | $count = $query->rowCount(); 32 | 33 | $progress = $this->getHelperSet()->get('progress'); 34 | $progress->start($output, $count); 35 | 36 | while (($module = $query->fetch()) !== false) { 37 | $found = false; 38 | 39 | $symname = $module['name']; 40 | if (stripos($symname, '.pdb') == strlen($symname) - 4) { 41 | $symname = substr($symname, 0, -4); 42 | } 43 | 44 | foreach ($symbols as $path) { 45 | if (file_exists($app['root'] . '/symbols/' . $path . '/' . $module['name'] . '/' . $module['identifier'] . '/' . $symname . '.sym.gz')) { 46 | $found = true; 47 | break; 48 | } 49 | } 50 | 51 | $progress->advance(); 52 | 53 | if (!$found && !$input->getOption('clean')) { 54 | continue; 55 | } 56 | 57 | $app['db']->executeUpdate('UPDATE module SET present = ? WHERE name = ? AND identifier = ?', array($found, $module['name'], $module['identifier'])); 58 | } 59 | 60 | $progress->finish(); 61 | 62 | $output->writeln('Waiting for processing lock...'); 63 | 64 | $lock = \PhutilFileLock::newForPath($app['root'] . '/cache/process.lck'); 65 | $lock->lock(300); 66 | 67 | $app['redis']->del('throttle:cache:symbol'); 68 | 69 | $output->writeln('Flushed symbol cache'); 70 | 71 | $lock->unlock(); 72 | } 73 | } 74 | 75 | -------------------------------------------------------------------------------- /src/Throttle/Command/UserUpdateCommand.php: -------------------------------------------------------------------------------- 1 | setName('user:update') 15 | ->setDescription('Update user information from Steam.'); 16 | } 17 | 18 | protected function execute(InputInterface $input, OutputInterface $output) 19 | { 20 | $app = $this->getApplication()->getContainer(); 21 | 22 | if (!$app['config']['apikey']) { 23 | throw new \Exception('Steam Community API Key not configured'); 24 | } 25 | 26 | $users = $app['db']->executeQuery('SELECT id FROM user WHERE updated IS NULL OR updated < DATE_SUB(NOW(), INTERVAL 1 DAY)'); 27 | 28 | $futures = array(); 29 | while (($user = $users->fetchColumn(0)) !== false) { 30 | $futures[$user] = new \HTTPSFuture('https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key=' . $app['config']['apikey'] . '&steamids=' . $user); 31 | } 32 | 33 | $count = count($futures); 34 | 35 | $output->writeln('Found ' . $count . ' stale user(s)'); 36 | 37 | $progress = $this->getHelperSet()->get('progress'); 38 | $progress->start($output, $count); 39 | 40 | $statsUpdated = 0; 41 | $statsFailed = 0; 42 | 43 | foreach (id(new \FutureIterator($futures))->limit(5) as $user => $future) { 44 | list($status, $body, $headers) = $future->resolve(); 45 | 46 | $progress->advance(); 47 | 48 | if ($status->isError()) { 49 | $statsFailed++; 50 | 51 | continue; 52 | } 53 | 54 | $data = json_decode($body); 55 | 56 | if ($data === null || empty($data->response->players)) { 57 | $app['db']->executeUpdate('UPDATE user SET updated = NOW() WHERE id = ?', array($user)); 58 | 59 | $statsFailed++; 60 | 61 | continue; 62 | } 63 | 64 | $data = $data->response->players[0]; 65 | 66 | if (!isset($data->avatarfull) || !isset($data->personaname)) { 67 | $app['db']->executeUpdate('UPDATE user SET updated = NOW() WHERE id = ?', array($user)); 68 | 69 | $statsFailed++; 70 | 71 | continue; 72 | } 73 | 74 | // Valve don't know how to unicode. 75 | $data->personaname = mb_convert_encoding($data->personaname, 'utf-8', 'utf-8'); 76 | $data->personaname = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $data->personaname); 77 | 78 | // Valve don't know how to HTTPS. 79 | $data->avatarfull = str_replace('http://cdn.akamai.steamstatic.com/', 'https://steamcdn-a.akamaihd.net/', $data->avatarfull); 80 | 81 | $app['db']->executeUpdate('UPDATE user SET name = ?, avatar = ?, updated = NOW() WHERE id = ?', array($data->personaname, $data->avatarfull, $user)); 82 | 83 | $statsUpdated++; 84 | } 85 | 86 | $progress->finish(); 87 | 88 | $app['redis']->hIncrBy('throttle:stats', 'users:updated', $statsUpdated); 89 | $app['redis']->hIncrBy('throttle:stats', 'users:failed', $statsFailed); 90 | } 91 | } 92 | 93 | -------------------------------------------------------------------------------- /src/Throttle/Home.php: -------------------------------------------------------------------------------- 1 | query->get('id'); 14 | if (isset($id)) { 15 | $crashid = strtolower(str_replace('-', '', $id)); 16 | 17 | try { 18 | $app['session']->getFlashBag()->set('internal', 'true'); 19 | 20 | return $app->redirect($app['url_generator']->generate('details', array('id' => $crashid))); 21 | } catch (\Exception $e) { 22 | try { 23 | return $app->redirect($app['url_generator']->generate('details_uuid', array('uuid' => $id))); 24 | } catch (\Exception $e) { 25 | $app['session']->getFlashBag()->add('error_crash', 'Invalid Crash ID.'); 26 | return $app->redirect($app['url_generator']->generate('index')); 27 | } 28 | } 29 | } 30 | 31 | return $app['twig']->render('index.html.twig', array( 32 | 'maintenance_message' => $app['config']['maintenance'], 33 | )); 34 | } 35 | 36 | public function login(Application $app) 37 | { 38 | $errorReturnUrl = $app['request']->get('return', $app['url_generator']->generate('index')); 39 | 40 | if (!$app['openid']->mode) { 41 | $app['openid']->identity = 'https://steamcommunity.com/openid'; 42 | 43 | try { 44 | return $app->redirect($app['openid']->authUrl()); 45 | } catch (\ErrorException $e) { 46 | $app['session']->getFlashBag()->add('error_auth', 'Unfortunately Steam Community seems to be having trouble staying online.'); 47 | return $app->redirect($errorReturnUrl); 48 | } 49 | } 50 | 51 | if ($app['openid']->mode == 'cancel') { 52 | $app['session']->getFlashBag()->add('error_auth', 'Authentication was cancelled.'); 53 | return $app->redirect($errorReturnUrl); 54 | } 55 | 56 | try { 57 | if (!$app['openid']->validate()) { 58 | $app['session']->getFlashBag()->add('error_auth', 'There was a problem during authentication.'); 59 | return $app->redirect($errorReturnUrl); 60 | } 61 | } catch (\ErrorException $e) { 62 | $app['session']->getFlashBag()->add('error_auth', 'Unfortunately Steam Community seems to be having trouble staying online.'); 63 | return $app->redirect($errorReturnUrl); 64 | } 65 | 66 | $id = preg_replace('/^https?\:\/\/steamcommunity\.com\/openid\/id\//', '', $app['openid']->identity); 67 | 68 | $app['db']->executeUpdate('INSERT IGNORE INTO user (id) VALUES (?)', array($id)); 69 | 70 | $app['session']->set('user', array( 71 | 'version' => self::SESSION_VERSION, 72 | 'id' => $id, 73 | )); 74 | 75 | $returnUrl = $app['request']->get('return', $app['url_generator']->generate('dashboard')); 76 | return $app->redirect($returnUrl); 77 | } 78 | 79 | public function logout(Application $app) 80 | { 81 | $app['session']->remove('user'); 82 | 83 | $returnUrl = $app['request']->get('return', $app['url_generator']->generate('index')); 84 | return $app->redirect($returnUrl); 85 | } 86 | } 87 | 88 | -------------------------------------------------------------------------------- /src/Throttle/Sharing.php: -------------------------------------------------------------------------------- 1 | abort(401); 13 | } 14 | 15 | $sharing = $app['db']->executeQuery('SELECT share.user AS id, user.name, user.avatar, accepted FROM share LEFT JOIN user ON share.user = user.id WHERE share.owner = ? ORDER BY accepted IS NULL DESC, accepted DESC', array($app['user']['id']))->fetchAll(); 16 | $shared = $app['db']->executeQuery('SELECT share.owner AS id, user.name, user.avatar, accepted FROM share LEFT JOIN user ON share.owner = user.id WHERE share.user = ? ORDER BY accepted IS NULL DESC, accepted DESC', array($app['user']['id']))->fetchAll(); 17 | 18 | return $app['twig']->render('share.html.twig', [ 19 | 'sharing' => $sharing, 20 | 'shared' => $shared, 21 | ]); 22 | } 23 | 24 | public function invite(Application $app) 25 | { 26 | if (!$app['user']) { 27 | $app->abort(401); 28 | } 29 | 30 | return $app['twig']->render('invite.html.twig'); 31 | } 32 | 33 | public function invite_post(Application $app) 34 | { 35 | if (!$app['user']) { 36 | $app->abort(401); 37 | } 38 | 39 | $user = $app['request']->get('user', null); 40 | if ($user === null) { 41 | $app['session']->getFlashBag()->add('error_share_invite', 'Missing Steam ID'); 42 | return $app->redirect($app['url_generator']->generate('share_invite')); 43 | } 44 | 45 | if (!ctype_digit($user) || gmp_cmp(gmp_and($user, '0xFFFFFFFF00000000'), '76561197960265728') !== 0) { 46 | $app['session']->getFlashBag()->add('error_share_invite', 'Invalid Steam ID'); 47 | return $app->redirect($app['url_generator']->generate('share_invite')); 48 | } 49 | 50 | if ($user === $app['user']['id']) { 51 | $app['session']->getFlashBag()->add('error_share_invite', 'You already have full access to your own reports'); 52 | return $app->redirect($app['url_generator']->generate('share_invite')); 53 | } 54 | 55 | $query = $app['db']->executeQuery('SELECT accepted FROM share WHERE owner = ? AND user = ?', array($app['user']['id'], $user))->fetch(); 56 | if ($query !== false) { 57 | if ($query['accepted'] !== null) { 58 | $app['session']->getFlashBag()->add('error_share_invite', 'You have already granted that user access'); 59 | } else { 60 | $app['session']->getFlashBag()->add('error_share_invite', 'You have already invited that user, but they have not accepted yet'); 61 | } 62 | return $app->redirect($app['url_generator']->generate('share_invite')); 63 | } 64 | 65 | $app['db']->executeUpdate('INSERT IGNORE INTO user (id) VALUES (?)', array($user)); 66 | $app['db']->executeUpdate('INSERT INTO share (owner, user) VALUES (?, ?)', array($app['user']['id'], $user)); 67 | 68 | $return = $app['request']->get('return', null); 69 | if (!$return) { 70 | $return = $app['url_generator']->generate('share'); 71 | } 72 | 73 | return $app->redirect($return); 74 | } 75 | 76 | public function accept(Application $app) 77 | { 78 | if (!$app['user']) { 79 | $app->abort(401); 80 | } 81 | 82 | $owner = $app['request']->get('owner', null); 83 | if ($owner === null || !ctype_digit($owner) || gmp_cmp(gmp_and($owner, '0xFFFFFFFF00000000'), '76561197960265728') !== 0) { 84 | throw new \Exception('Missing or invalid target'); 85 | } 86 | 87 | $app['db']->executeUpdate('UPDATE share SET accepted = NOW() WHERE owner = ? AND user = ?', array($owner, $app['user']['id'])); 88 | 89 | $return = $app['request']->get('return', null); 90 | if (!$return) { 91 | $return = $app['url_generator']->generate('share'); 92 | } 93 | 94 | return $app->redirect($return); 95 | } 96 | 97 | public function revoke(Application $app) 98 | { 99 | if (!$app['user']) { 100 | $app->abort(401); 101 | } 102 | 103 | $user = $app['request']->get('user', null); 104 | $owner = $app['request']->get('owner', null); 105 | if ($user === $owner || ($user !== null && $owner !== null)) { 106 | throw new \Exception('Missing or multiple targets'); 107 | } 108 | 109 | if ($user !== null) { 110 | if (!ctype_digit($user) || gmp_cmp(gmp_and($user, '0xFFFFFFFF00000000'), '76561197960265728') !== 0) { 111 | throw new \Exception('Invalid target'); 112 | } 113 | 114 | $app['db']->executeUpdate('DELETE FROM share WHERE owner = ? AND user = ?', array($app['user']['id'], $user)); 115 | } else if ($owner !== null) { 116 | if (!ctype_digit($owner) || gmp_cmp(gmp_and($owner, '0xFFFFFFFF00000000'), '76561197960265728') !== 0) { 117 | throw new \Exception('Invalid target'); 118 | } 119 | 120 | $app['db']->executeUpdate('DELETE FROM share WHERE owner = ? AND user = ?', array($owner, $app['user']['id'])); 121 | } 122 | 123 | $return = $app['request']->get('return', null); 124 | if (!$return) { 125 | $return = $app['url_generator']->generate('share'); 126 | } 127 | 128 | return $app->redirect($return); 129 | } 130 | } 131 | 132 | -------------------------------------------------------------------------------- /src/Throttle/Subscription.php: -------------------------------------------------------------------------------- 1 | request->all(); 28 | 29 | if (!isset($params['p_signature'])) { 30 | $app->abort(403, 'Missing signature'); 31 | } 32 | 33 | $signature = base64_decode($params['p_signature']); 34 | unset($params['p_signature']); 35 | 36 | ksort($params); 37 | foreach ($params as $k => $v) { 38 | if(!in_array(gettype($v), array('object', 'array'))) { 39 | $params[$k] = (string)$v; 40 | } 41 | } 42 | $params = serialize($params); 43 | 44 | if (!openssl_verify($params, $signature, self::PUBLIC_KEY, OPENSSL_ALGO_SHA1)) { 45 | $app->abort(403, 'Invalid signature'); 46 | } 47 | 48 | return true; 49 | } 50 | 51 | private static function getPriceAdjustmentTable() 52 | { 53 | $output = []; 54 | 55 | for ($i = 0; $i <= 120; ++$i) { 56 | $price = number_format($i, 2, '.', ''); 57 | $output[] = [ 58 | 'price' => $price, 59 | 'monthly' => md5($price . self::MONTHLY_SECRET_KEY), 60 | 'quarterly' => md5($price . self::QUARTERLY_SECRET_KEY), 61 | 'yearly' => md5($price . self::YEARLY_SECRET_KEY), 62 | ]; 63 | } 64 | 65 | return $output; 66 | } 67 | 68 | public function webhook(Application $app) 69 | { 70 | self::validateWebhook($app); 71 | 72 | $log = new \Monolog\Logger('throttle.paddle'); 73 | $log->pushHandler(new \Monolog\Handler\StreamHandler($app['root'].'/logs/paddle.log')); 74 | 75 | $params = $app['request']->request; 76 | 77 | $paramArray = $app['request']->request->all(); 78 | unset($paramArray['p_signature']); 79 | $log->info('Webhook received!', ['request' => $paramArray]); 80 | unset($paramArray); 81 | 82 | switch ($params->get('alert_name')) { 83 | case 'subscription_created'; 84 | break; 85 | case 'subscription_updated'; 86 | break; 87 | case 'subscription_cancelled'; 88 | break; 89 | case 'subscription_payment_succeeded'; 90 | break; 91 | case 'subscription_payment_failed'; 92 | break; 93 | case 'subscription_payment_refunded'; 94 | break; 95 | } 96 | 97 | return ''; 98 | } 99 | 100 | public function subscribe(Application $app) 101 | { 102 | if (!$app['feature']['subscriptions']) { 103 | $app->abort(404); 104 | } 105 | 106 | if (!$app['user']) { 107 | $app->abort(401); 108 | } 109 | 110 | return $app['twig']->render('subscribe.html.twig', [ 111 | 'price_table' => self::getPriceAdjustmentTable(), 112 | ]); 113 | } 114 | } 115 | 116 | -------------------------------------------------------------------------------- /src/Throttle/Symbols.php: -------------------------------------------------------------------------------- 1 | get('symbol_file'); 12 | if ($data === null) { 13 | $data = $app['request']->getContent(); 14 | } 15 | 16 | $app['redis']->hIncrBy('throttle:stats', 'symbols:submitted', 1); 17 | $app['redis']->hIncrBy('throttle:stats', 'symbols:submitted:bytes', strlen($data)); 18 | 19 | $lines = phutil_split_lines($data, false); 20 | 21 | if (!preg_match('/^MODULE (?P[^ ]++) (?P[^ ]++) (?P[a-fA-F0-9]++) (?P[^\\/\\\\\r\n]++)$/m', $lines[0], $info)) { 22 | $app['monolog']->warning('Invalid symbol file: ' . $lines[0]); 23 | $app['redis']->hIncrBy('throttle:stats', 'symbols:rejected:invalid', 1); 24 | 25 | return new \Symfony\Component\HttpFoundation\Response('Invalid symbol file', 400); 26 | } 27 | 28 | if ($info['operatingsystem'] === 'Linux') { 29 | $functions = 0; 30 | 31 | foreach ($lines as $line) { 32 | list($type) = explode(' ', $line, 2); 33 | 34 | if ($type === 'STACK') { 35 | break; 36 | } 37 | 38 | if ($type === 'FUNC') { 39 | $functions++; 40 | } 41 | } 42 | 43 | if ($functions === 0) { 44 | $app['redis']->hIncrBy('throttle:stats', 'symbols:rejected:no-functions', 1); 45 | return new \Symfony\Component\HttpFoundation\Response('Symbol file had no FUNC records, please update to Accelerator 2.4.3 or later', 400); 46 | } 47 | } 48 | 49 | $path = $app['root'] . '/symbols/public/' . $info['name'] . '/' . $info['id']; 50 | 51 | \Filesystem::createDirectory($path, 0755, true); 52 | 53 | $file = $info['name']; 54 | if (pathinfo($file, PATHINFO_EXTENSION) == 'pdb') { 55 | $file = substr($file, 0, -4); 56 | } 57 | 58 | \Filesystem::writeFile($path . '/' . $file . '.sym.gz', gzencode($data)); 59 | 60 | $app['redis']->hIncrBy('throttle:stats', 'symbols:accepted', 1); 61 | 62 | return $app['twig']->render('submit-symbols.txt.twig', array( 63 | 'module' => $info, 64 | )); 65 | } 66 | } 67 | 68 | -------------------------------------------------------------------------------- /symbols/public/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asherkin/throttle/f150edb423ce2f805b7c20d21dcc6887b9128cd1/symbols/public/.gitignore -------------------------------------------------------------------------------- /views/carburetor.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html.twig' %} 2 | 3 | {% set logout_to_index = true %} 4 | {% set supports_mobile = true %} 5 | 6 | {% block content %} 7 | 11 |
12 | 24 | {% endblock %} 25 | 26 | {% block scripts %} 27 | 28 | 29 | 98 | {% endblock %} 99 | 100 | -------------------------------------------------------------------------------- /views/console.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html.twig' %} 2 | 3 | {% set logout_to_index = true %} 4 | {% set supports_mobile = true %} 5 | 6 | {% block content %} 7 | 10 |
11 |
12 | {% for line in console %} 13 | 14 | 15 | 16 | 17 | 18 | {% else %} 19 | 20 | 21 | 22 | {% endfor %} 23 |
{{ line[3]|trim }}
Console output is unavailable for this crash.
24 | 25 | {% endblock %} 26 | 27 | -------------------------------------------------------------------------------- /views/dashboard.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html.twig' %} 2 | 3 | {% set logout_to_index = true %} 4 | 5 | {% block content %} 6 | {% if shared|length > 1 or app.user.admin %} 7 |
8 | 22 |
23 | {% endif %} 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | {% set last = null %} 38 | {% for crash in crashes %} 39 | {% set last = crash.timestamp %} 40 | 41 | 46 | 47 | 48 | {% spaceless %} 49 | 56 | {% endspaceless %} 57 | {% if crash.module is not empty or crash.module2 is empty %} 58 | {% set frame = {rendered: crash.rendered, first: true} %} 59 | {% else %} 60 | {% set frame = {rendered: crash.rendered2, first: false} %} 61 | {% endif %} 62 | 65 | {% spaceless %} 66 | 82 | {% endspaceless %} 83 | 84 | {% else %} 85 | 86 | 87 | 88 | {% endfor %} 89 | 90 | 91 | 92 | 102 | 103 | 104 |
Crash IDUploaded
42 | {% if crash.cmdline %} 43 | 44 | {% endif %} 45 | {{ crash.id|crashid }}{{ crash.timestamp|diffdate }}
No Crashes
93 | 101 |
105 |
106 | {% endblock %} 107 | 108 | -------------------------------------------------------------------------------- /views/details.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html.twig' %} 2 | 3 | {% set supports_mobile = true %} 4 | 5 | {% block content %} 6 | {% if not crash.owner %} 7 | {% if app.config.accelerator %} 8 |
This crash was submitted anonymously. If you are the owner, please configure Accelerator with your Steam ID.
9 | {% endif %} 10 | {% else %} 11 | {% if not app.user %} 12 |
If you are the owner of this crash, login to see more information.
13 | {% elseif not can_manage %} 14 |
You can not see full information about this crash as you are not the owner.
15 | {% endif %} 16 | {% endif %} 17 | 18 |
19 |
20 |
Uploaded
21 |
{{ crash.timestamp|date }}
22 | {% if can_manage %} 23 | {% if crash.ip %} 24 |
Upload IP
25 |
{{ crash.ip }}
26 | {% endif %} 27 | {% if can_manage and crash.owner and crash.owner != app.user.id %} 28 |
Owner
29 |
{{ crash.name ?? crash.owner }}
30 | {% endif %} 31 | {% if crash.cmdline %} 32 |
Command Line
33 |
{{ crash.cmdline }}
34 | {% endif %} 35 | {% for key, value in crash.metadata %} 36 |
{{ key|format_metadata_key }}
37 | {% if value %} 38 |
{{ value }}
39 | {% else %} 40 |
 
41 | {% endif %} 42 | {% endfor %} 43 | {% endif %} 44 |
45 | {% if app.feature.subscriptions %} 46 | Supporter 47 | {% endif %} 48 |
49 | 50 | {% if crash.processed == 0 %} 51 |
This crash is pending processing, try again later.
52 | {% elseif crash.failed == 1 %} 53 |
An error occurred while processing this crash.
54 | {% endif %} 55 | 56 | {%if outdated %} 57 |
This crash was submitted using an older version of Accelerator, please update to the latest version for full functionality.
58 | {% endif %} 59 | 60 | {% if crash.processed == 1 and crash.failed == 0 %} 61 | {% for notice in notices %} 62 |
{{ notice.text|raw }}
63 | {% endfor %} 64 | 65 | {%if show_sourcepawn_message %} 66 |
Update to SourceMod 1.10.0.6431 or later for improved SourcePawn stack traces.
This crash is from a server running a version of SourceMod that does not fully support SourcePawn stack walking and the stack trace displayed may be incomplete or incorrect.
67 | {% endif %} 68 | 69 | {% if crash.thread != 0 %} 70 |
This crash is not from the main thread.
The stack trace displayed below is of the thread that crashed.
71 | {% endif %} 72 | 73 | {% if has_error_string and can_manage %} 74 |
75 | {% endif %} 76 | 77 | {% if modules|length == 0 %} 78 |
This crash is missing a module list and will not have symbol information.
79 | {% endif %} 80 | 81 |
82 | {% if stats['crashes'] != 1 %} 83 | {{ stats['crashes'] }} reports share this crash signature. ({{ stats['owners'] }} owner{% if stats['owners'] != 1 %}s{% endif %} / {{ stats['ips'] }} ip{% if stats['ips'] != 1 %}s{% endif %}) 84 | {% else %} 85 | This crash signature is unique. 86 | {% endif %} 87 |
88 | {% endif %} 89 | 90 | {% if can_manage %} 91 |
92 | Download Minidump 93 | {% if crash.has_console_log %} 94 | View Console 95 | {% endif %} 96 |
97 | View Raw 98 | {% if app.user.admin and crash.processed == 1 %} 99 | {# View Processing Logs #} 100 |
101 | 102 |
103 | {% endif %} 104 |
105 | 106 |
107 |
108 |
109 | {% endif %} 110 | 111 | {% if crash.processed == 1 and crash.failed == 0 %} 112 |

Stack Trace

113 |
114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | {% for frame in stack %} 123 | 124 | 125 | 134 | 135 | {% endfor %} 136 | 137 | 138 | 139 | 140 | 141 | 142 |
Function
{{ frame.frame }} 126 | {{ frame.rendered }} 127 | {% if frame.url %} 128 | 129 | {% endif %} 130 | {% if '__SourceHook_' in frame.rendered %} 131 | 132 | {% endif %} 133 |
143 |
144 | 145 | {% if modules|length > 0 and can_manage %} 146 |

Modules

147 |
148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | {% for module in modules %} 158 | {% set class = ((module.identifier == '000000000000000000000000000000000') ? 'info' : (module.processed ? '' : (module.present ? 'warning' : 'danger'))) %} 159 | 160 | 161 | 162 | 163 | 164 | {% endfor %} 165 | 166 | 167 | 168 | 169 | 170 | 171 |
NameIdentifierBase
{{ module.name }}{{ module.identifier }}{{ module.base|address }}
172 |
173 | {% endif %} 174 | {% endif %} 175 | 187 | {% endblock %} 188 | 189 | {% block scripts %} 190 | 191 | 225 | {% endblock %} 226 | 227 | -------------------------------------------------------------------------------- /views/error.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html.twig' %} 2 | 3 | {% block content %} 4 |
5 | 6 | {{ title }} 7 | {{ comment | nl2br }} 8 |
9 | {% endblock %} 10 | 11 | -------------------------------------------------------------------------------- /views/index.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html.twig' %} 2 | 3 | {% set supports_mobile = true %} 4 | 5 | {% block content %} 6 | {% if maintenance_message is not empty %} 7 |
8 |
9 |
{{ maintenance_message|raw }}
10 |
11 |
12 | {% endif %} 13 |
14 |
15 |
  Crash reports received to date
16 |
17 |
18 |
  Last 24 hours (5m average)
19 |
20 |
21 |
22 |
23 |
24 | {% for error in app.session.flashbag.get('error_crash') %} 25 |
{{ error }}
26 | {% endfor %} 27 |
28 |
29 | Crash ID 30 | 31 | 32 | 33 | 34 |
35 |
36 | 37 | {% if app.user %} 38 | View Dashboard 39 | {% else %} 40 | Login 41 | {% endif %} 42 |
43 |
44 |
45 | {% endblock %} 46 | 47 | {% block scripts %} 48 | {% include 'stat-counter.stub.twig' %} 49 | 55 | {% endblock %} 56 | -------------------------------------------------------------------------------- /views/invite.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html.twig' %} 2 | 3 | {% set logout_to_index = true %} 4 | {% set supports_mobile = true %} 5 | 6 | {% block content %} 7 |
8 |
9 |
10 | Back to Sharing 11 |
12 |
13 |
14 |

Granting access to your crash reports using this form will give full permissions to the other person to interact with them as you do.

15 |

This includes downloading the raw minidump and viewing the full command line options.

16 |

If you just want to share the stack trace and other limited public information with someone, just link them to the crash report page rather than adding them here.

17 |
18 | {% for error in app.session.flashbag.get('error_share_invite') %} 19 |
{{ error }}
20 | {% endfor %} 21 |
22 |
23 | Steam ID 24 | 25 | 26 | 27 | 28 |
29 |
30 |
31 |
32 |
33 | {% endblock %} 34 | 35 | -------------------------------------------------------------------------------- /views/layout.html.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {% if supports_mobile|default(false) %} 7 | 8 | {% endif %} 9 | Throttle 10 | 11 | 12 | {% block stylesheets %}{% endblock %} 13 | 14 | {% if 'Kindle/3.0' in app.request.headers.get('User-Agent') %} 15 | 16 | {% endif %} 17 | 18 | 19 | 58 |
59 | {% for error in app.session.flashbag.get('error_auth') %} 60 |
{{ error }}
61 | {% endfor %} 62 | {% block content %}{% endblock %} 63 |
64 | 67 | 68 | 69 | 74 | {% block scripts %}{% endblock %} 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /views/logs.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html.twig' %} 2 | 3 | {% set logout_to_index = true %} 4 | {% set supports_mobile = true %} 5 | 6 | {% block content %} 7 |
8 | Back to Details 9 |
10 | {% if logs is not empty %} 11 |
{{ logs }}
12 | {% else %} 13 |
Logs are unavailable for this crash.
14 | {% endif %} 15 | {% endblock %} 16 | 17 | -------------------------------------------------------------------------------- /views/share.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html.twig' %} 2 | 3 | {% set logout_to_index = true %} 4 | {% set supports_mobile = true %} 5 | 6 | {% block content %} 7 |
8 |
9 |

Sharing With Others

10 |
11 | 12 | 13 | 14 | 15 | 16 | 21 | 22 | 23 | 24 | {% for user in sharing %} 25 | 26 | 31 | 34 | 44 | 45 | {% else %} 46 | 47 | 48 | 49 | {% endfor %} 50 | 51 | 52 | 53 | 56 | 57 | 58 |
User 17 | {# 18 | Invite 19 | #} 20 |
27 | 28 | {{ user.name ?? user.id }}'s avatar 29 | 30 | 32 | {{ user.name ?? user.id }} 33 | 35 | {% if user.accepted == null %} 36 | Pending 37 | {% else %} 38 | {{ user.accepted|date('Y-m-d') }} 39 | {% endif %} 40 |
41 | 42 |
43 |
You aren't sharing with anybody
54 | Invite 55 |
59 |
60 | 61 |

Shared With You

62 |
63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | {% for user in shared %} 73 | 74 | 79 | 82 | 94 | 95 | {% else %} 96 | 97 | 98 | 99 | {% endfor %} 100 | 101 |
User
75 | 76 | {{ user.name ?? user.id }}'s avatar 77 | 78 | 80 | {{ user.name ?? user.id }} 81 | 83 | {% if user.accepted == null %} 84 |
85 | 86 |
87 | {% else %} 88 | {{ user.accepted|date('Y-m-d') }} 89 | {% endif %} 90 |
91 | 92 |
93 |
Nobody is sharing with you
102 |
103 |
104 |
105 | {% endblock %} 106 | 107 | -------------------------------------------------------------------------------- /views/stat-counter.stub.twig: -------------------------------------------------------------------------------- 1 | 51 | -------------------------------------------------------------------------------- /views/stats.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html.twig' %} 2 | 3 | {% block content %} 4 | {% if module %} 5 |
6 | 16 |
17 | {% else %} 18 |
19 |
20 |
  Unique servers last 24 hours
21 |
22 |
23 |
  Crash reports received to date
24 |
25 |
26 |
  Last 24 hours (5m average)
27 |
28 |
29 | {% endif %} 30 |

Crash Reports / Hour{% if module %} Historical data may be inaccurate due to active filter{% endif %}

31 |
32 |
33 |
34 | 35 |
36 |
37 |
38 |

Crash Reports / Day{% if module %} Historical data may be inaccurate due to active filter{% endif %}

39 |
40 |
41 |
42 | 43 |
44 |
45 |
46 |

47 | Top Crashers 48 | 49 | 50 | 51 | 52 | 53 | 54 |

55 |
56 | 57 | 58 | 59 | 60 | 63 | 64 | 65 |
Function
61 | 62 |
66 |
67 |

Latest Crash Reports

68 |
69 | 70 | 71 | 72 | 73 | 76 | 77 | 78 |
Function
74 | 75 |
79 |
80 | {% if not module %} 81 |

Processing Performance

82 |
83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 |
92 | {% endif %} 93 | {% endblock %} 94 | 95 | {% block scripts %} 96 | {% include 'stat-counter.stub.twig' %} 97 | 98 | 99 | 302 | {% endblock %} 303 | 304 | -------------------------------------------------------------------------------- /views/submit-empty.txt.twig: -------------------------------------------------------------------------------- 1 | A problem with your host is preventing crash reports from being generated. 2 | -------------------------------------------------------------------------------- /views/submit-nosteam.txt.twig: -------------------------------------------------------------------------------- 1 | Servers that support piracy are not welcome. -------------------------------------------------------------------------------- /views/submit-reject.txt.twig: -------------------------------------------------------------------------------- 1 | Rate limit exceeded. -------------------------------------------------------------------------------- /views/submit-symbols.txt.twig: -------------------------------------------------------------------------------- 1 | Stored symbols for {{ module.name }}/{{ module.id }}/{{ module.operatingsystem }}/{{ module.architecture }} 2 | -------------------------------------------------------------------------------- /views/submit.txt.twig: -------------------------------------------------------------------------------- 1 | Crash ID: {{ id|crashid }} -------------------------------------------------------------------------------- /views/subscribe.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html.twig' %} 2 | 3 | {% set logout_to_index = true %} 4 | {% set supports_mobile = true %} 5 | 6 | {% block stylesheets %} 7 | 8 | {% endblock %} 9 | 10 | {% block content %} 11 | {% set supporter_count = 1 %} 12 | {% set supporter_total = 20 %} 13 |
14 |
15 |
16 | {{ supporter_count|number_format(0) }} supporter{% if supporter_count == 1 %} is{% else %}s are {% endif %} contributing 17 | ${{ supporter_total|number_format(2) }} per month 18 |
19 |

Money raised by Throttle Supporters goes towards hosting and developing this service.
Your status as a Throttle Supporter will be publicly displayed on all crash dumps associated with your Steam ID.

20 |
21 |
22 |
23 |
24 |
25 | 42 |
43 |
44 |
45 | -% 46 |
47 | Tax that goes to your government. 48 |
49 |
50 |
51 | -% 52 |
53 | Fees for the payment processor. 54 |
55 |
56 |
57 | +% 58 |
59 | Percentage that goes to support Throttle! 60 |
61 |
62 | 63 |
64 |
65 |
66 |
67 |
68 | Subscription can be cancelled at any time, and access to any subscription-specific functionality will remain until end of the current billing period. If you wish to make a one-off contribution, please subscribe for the amount of your choosing and then cancel once the first payment is confirmed. If anything goes wrong, I'm here to help. 69 |
70 |
71 | {% endblock %} 72 | 73 | {% block scripts %} 74 | 75 | 76 | 229 | {% endblock %} 230 | 231 | -------------------------------------------------------------------------------- /views/view.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html.twig' %} 2 | 3 | {% set logout_to_index = true %} 4 | {% set supports_mobile = true %} 5 | 6 | {% block content %} 7 |
8 | Back to Details 9 | Back to View Raw 10 |
11 |
12 |
13 |
14 | {% endblock %} 15 | 16 | {% block scripts %} 17 | 18 | 19 | {% endblock %} 20 | 21 | -------------------------------------------------------------------------------- /web/css/bootstrap-slider.min.css: -------------------------------------------------------------------------------- 1 | /*! ======================================================= 2 | VERSION 9.9.0 3 | ========================================================= */ 4 | /*! ========================================================= 5 | * bootstrap-slider.js 6 | * 7 | * Maintainers: 8 | * Kyle Kemp 9 | * - Twitter: @seiyria 10 | * - Github: seiyria 11 | * Rohit Kalkur 12 | * - Twitter: @Rovolutionary 13 | * - Github: rovolution 14 | * 15 | * ========================================================= 16 | * 17 | * bootstrap-slider is released under the MIT License 18 | * Copyright (c) 2017 Kyle Kemp, Rohit Kalkur, and contributors 19 | * 20 | * Permission is hereby granted, free of charge, to any person 21 | * obtaining a copy of this software and associated documentation 22 | * files (the "Software"), to deal in the Software without 23 | * restriction, including without limitation the rights to use, 24 | * copy, modify, merge, publish, distribute, sublicense, and/or sell 25 | * copies of the Software, and to permit persons to whom the 26 | * Software is furnished to do so, subject to the following 27 | * conditions: 28 | * 29 | * The above copyright notice and this permission notice shall be 30 | * included in all copies or substantial portions of the Software. 31 | * 32 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 33 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 34 | * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 35 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 36 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 37 | * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 38 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 39 | * OTHER DEALINGS IN THE SOFTWARE. 40 | * 41 | * ========================================================= */.slider{display:inline-block;vertical-align:middle;position:relative}.slider.slider-horizontal{width:210px;height:20px}.slider.slider-horizontal .slider-track{height:10px;width:100%;margin-top:-5px;top:50%;left:0}.slider.slider-horizontal .slider-selection,.slider.slider-horizontal .slider-track-low,.slider.slider-horizontal .slider-track-high{height:100%;top:0;bottom:0}.slider.slider-horizontal .slider-tick,.slider.slider-horizontal .slider-handle{margin-left:-10px}.slider.slider-horizontal .slider-tick.triangle,.slider.slider-horizontal .slider-handle.triangle{position:relative;top:50%;transform:translateY(-50%);border-width:0 10px 10px 10px;width:0;height:0;border-bottom-color:#2e6da4;margin-top:0}.slider.slider-horizontal .slider-tick-container{white-space:nowrap;position:absolute;top:0;left:0;width:100%}.slider.slider-horizontal .slider-tick-label-container{white-space:nowrap;margin-top:20px}.slider.slider-horizontal .slider-tick-label-container .slider-tick-label{padding-top:4px;display:inline-block;text-align:center}.slider.slider-horizontal.slider-rtl .slider-track{left:initial;right:0}.slider.slider-horizontal.slider-rtl .slider-tick,.slider.slider-horizontal.slider-rtl .slider-handle{margin-left:initial;margin-right:-10px}.slider.slider-horizontal.slider-rtl .slider-tick-container{left:initial;right:0}.slider.slider-vertical{height:210px;width:20px}.slider.slider-vertical .slider-track{width:10px;height:100%;left:25%;top:0}.slider.slider-vertical .slider-selection{width:100%;left:0;top:0;bottom:0}.slider.slider-vertical .slider-track-low,.slider.slider-vertical .slider-track-high{width:100%;left:0;right:0}.slider.slider-vertical .slider-tick,.slider.slider-vertical .slider-handle{margin-top:-10px}.slider.slider-vertical .slider-tick.triangle,.slider.slider-vertical .slider-handle.triangle{border-width:10px 0 10px 10px;width:1px;height:1px;border-left-color:#2e6da4;border-right-color:#2e6da4;margin-left:0;margin-right:0}.slider.slider-vertical .slider-tick-label-container{white-space:nowrap}.slider.slider-vertical .slider-tick-label-container .slider-tick-label{padding-left:4px}.slider.slider-vertical.slider-rtl .slider-track{left:initial;right:25%}.slider.slider-vertical.slider-rtl .slider-selection{left:initial;right:0}.slider.slider-vertical.slider-rtl .slider-tick.triangle,.slider.slider-vertical.slider-rtl .slider-handle.triangle{border-width:10px 10px 10px 0}.slider.slider-vertical.slider-rtl .slider-tick-label-container .slider-tick-label{padding-left:initial;padding-right:4px}.slider.slider-disabled .slider-handle{background-image:-webkit-linear-gradient(top,#dfdfdf 0,#bebebe 100%);background-image:-o-linear-gradient(top,#dfdfdf 0,#bebebe 100%);background-image:linear-gradient(to bottom,#dfdfdf 0,#bebebe 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdfdfdf',endColorstr='#ffbebebe',GradientType=0)}.slider.slider-disabled .slider-track{background-image:-webkit-linear-gradient(top,#e5e5e5 0,#e9e9e9 100%);background-image:-o-linear-gradient(top,#e5e5e5 0,#e9e9e9 100%);background-image:linear-gradient(to bottom,#e5e5e5 0,#e9e9e9 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe5e5e5',endColorstr='#ffe9e9e9',GradientType=0);cursor:not-allowed}.slider input{display:none}.slider .tooltip.top{margin-top:-36px}.slider .tooltip-inner{white-space:nowrap;max-width:none}.slider .hide{display:none}.slider-track{position:absolute;cursor:pointer;background-image:-webkit-linear-gradient(top,#f5f5f5 0,#f9f9f9 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#f9f9f9 100%);background-image:linear-gradient(to bottom,#f5f5f5 0,#f9f9f9 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5',endColorstr='#fff9f9f9',GradientType=0);-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);border-radius:4px}.slider-selection{position:absolute;background-image:-webkit-linear-gradient(top,#f9f9f9 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#f9f9f9 0,#f5f5f5 100%);background-image:linear-gradient(to bottom,#f9f9f9 0,#f5f5f5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff9f9f9',endColorstr='#fff5f5f5',GradientType=0);-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;border-radius:4px}.slider-selection.tick-slider-selection{background-image:-webkit-linear-gradient(top,#8ac1ef 0,#82b3de 100%);background-image:-o-linear-gradient(top,#8ac1ef 0,#82b3de 100%);background-image:linear-gradient(to bottom,#8ac1ef 0,#82b3de 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff8ac1ef',endColorstr='#ff82b3de',GradientType=0)}.slider-track-low,.slider-track-high{position:absolute;background:transparent;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;border-radius:4px}.slider-handle{position:absolute;top:0;width:20px;height:20px;background-color:#337ab7;background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7',endColorstr='#ff2e6da4',GradientType=0);filter:none;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 2px rgba(0,0,0,.05);border:0 solid transparent}.slider-handle.round{border-radius:50%}.slider-handle.triangle{background:transparent none}.slider-handle.custom{background:transparent none}.slider-handle.custom::before{line-height:20px;font-size:20px;content:'\2605';color:#726204}.slider-tick{position:absolute;width:20px;height:20px;background-image:-webkit-linear-gradient(top,#f9f9f9 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#f9f9f9 0,#f5f5f5 100%);background-image:linear-gradient(to bottom,#f9f9f9 0,#f5f5f5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff9f9f9',endColorstr='#fff5f5f5',GradientType=0);-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;filter:none;opacity:.8;border:0 solid transparent}.slider-tick.round{border-radius:50%}.slider-tick.triangle{background:transparent none}.slider-tick.custom{background:transparent none}.slider-tick.custom::before{line-height:20px;font-size:20px;content:'\2605';color:#726204}.slider-tick.in-selection{background-image:-webkit-linear-gradient(top,#8ac1ef 0,#82b3de 100%);background-image:-o-linear-gradient(top,#8ac1ef 0,#82b3de 100%);background-image:linear-gradient(to bottom,#8ac1ef 0,#82b3de 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff8ac1ef',endColorstr='#ff82b3de',GradientType=0);opacity:1} -------------------------------------------------------------------------------- /web/css/kindle.css: -------------------------------------------------------------------------------- 1 | * { 2 | color: black !important; 3 | background-color: transparent !important; 4 | } 5 | 6 | .icon-spin { 7 | -moz-animation: none; 8 | -o-animation: none; 9 | -webkit-animation: none; 10 | animation: none; 11 | } 12 | 13 | .highcharts-series rect { 14 | fill: black; 15 | } 16 | -------------------------------------------------------------------------------- /web/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asherkin/throttle/f150edb423ce2f805b7c20d21dcc6887b9128cd1/web/favicon.ico -------------------------------------------------------------------------------- /web/font/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asherkin/throttle/f150edb423ce2f805b7c20d21dcc6887b9128cd1/web/font/fontawesome-webfont.eot -------------------------------------------------------------------------------- /web/font/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asherkin/throttle/f150edb423ce2f805b7c20d21dcc6887b9128cd1/web/font/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /web/font/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asherkin/throttle/f150edb423ce2f805b7c20d21dcc6887b9128cd1/web/font/fontawesome-webfont.woff -------------------------------------------------------------------------------- /web/js/carburetor-analyze.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | if (!String.prototype.padStart) { 4 | String.prototype.padStart = function padStart(targetLength,padString) { 5 | targetLength = targetLength>>0; //floor if number or convert non-number to 0; 6 | padString = String(padString || ' '); 7 | if (this.length > targetLength) { 8 | return String(this); 9 | } 10 | else { 11 | targetLength = targetLength-this.length; 12 | if (targetLength > padString.length) { 13 | padString += padString.repeat(targetLength/padString.length); //append to original to ensure we are longer than needed 14 | } 15 | return padString.slice(0,targetLength) + String(this); 16 | } 17 | }; 18 | } 19 | 20 | if (!String.prototype.padEnd) { 21 | String.prototype.padEnd = function padEnd(targetLength,padString) { 22 | targetLength = targetLength>>0; //floor if number or convert non-number to 0; 23 | padString = String(padString || ' '); 24 | if (this.length > targetLength) { 25 | return String(this); 26 | } 27 | else { 28 | targetLength = targetLength-this.length; 29 | if (targetLength > padString.length) { 30 | padString += padString.repeat(targetLength/padString.length); //append to original to ensure we are longer than needed 31 | } 32 | return String(this) + padString.slice(0,targetLength); 33 | } 34 | }; 35 | } 36 | 37 | const FRAME_TRUST = [ 38 | 'unknown', 39 | 'stack scanning', 40 | 'call frame info with scanning', 41 | 'previous frame\'s frame pointer', 42 | 'call frame info', 43 | 'external stack walker', 44 | 'instruction pointer in context', 45 | ]; 46 | 47 | function print_registers(indent, registers) { 48 | let order = [ 49 | "eip", "esp", "ebp", "ebx", 50 | "esi", "edi", "eax", "ecx", 51 | "edx", "efl", 52 | "rax", "rdx", "rcx", "rbx", 53 | "rsi", "rdi", "rbp", "rsp", 54 | "r8", "r9", "r10", "r11", 55 | "r12", "r13", "r14", 56 | "r15", "rip", 57 | ]; 58 | 59 | let source = order; 60 | let printed = {}; 61 | let register_count = 0; 62 | let line = indent; 63 | 64 | for (let i = 0; i < 2; ++i) { 65 | for (let register in source) { 66 | if (i === 0) { 67 | register = order[register]; 68 | } 69 | 70 | if (printed[register] || registers[register] === undefined) { 71 | continue; 72 | } 73 | 74 | line += register + ': 0x' + registers[register].toString(16).padStart(8, '0'); 75 | 76 | register_count++; 77 | 78 | if (register_count < 4) { 79 | line += ' '; 80 | } else { 81 | console.log(line); 82 | 83 | register_count = 0; 84 | line = indent; 85 | } 86 | 87 | printed[register] = true; 88 | } 89 | 90 | source = registers; 91 | } 92 | 93 | if (register_count > 0) { 94 | console.log(line); 95 | } 96 | } 97 | 98 | function print_stack(indent, base, memory) { 99 | const kAddressBytes = 4; 100 | const kHeader = false; 101 | const kRowBytes = 16; 102 | const kChunkBytes = 8; 103 | const kChunkText = false; 104 | 105 | if (kHeader) { 106 | let line = indent + ' '.repeat(kAddressBytes * 2) + ' '; 107 | let string = ''; 108 | for (let i = 0; i < kRowBytes; ++i) { 109 | line += i.toString(16).padStart(2, ' ') + ' '; 110 | string += ' '; 111 | //string += i.toString(16).padStart(2, ' ').substr(1, 1); 112 | 113 | if (kChunkBytes > 0 && i < (kRowBytes - 1) && (i % kChunkBytes) === (kChunkBytes - 1)) { 114 | line += ' '; 115 | if (kChunkText) { 116 | string += ' '; 117 | } 118 | } 119 | } 120 | line += ' ' + string + ' '; 121 | console.log(line); 122 | } 123 | 124 | for (let offset = 0; offset < memory.length;) { 125 | let line = indent + (base + offset).toString(16).padStart(kAddressBytes * 2, '0') + ' '; 126 | 127 | let string = ''; 128 | for (let i = 0; i < kRowBytes; ++i, ++offset) { 129 | if (offset < memory.length) { 130 | line += memory[offset].toString(16).padStart(2, '0') + ' '; 131 | 132 | const character = String.fromCharCode(memory[offset]); 133 | string += (character.length === 1 && character.match(/^[ -~]$/)) ? character : '.'; 134 | } else { 135 | line += ' '; 136 | string += ' '; 137 | } 138 | 139 | if (kChunkBytes > 0 && i < (kRowBytes - 1) && (i % kChunkBytes) === (kChunkBytes - 1)) { 140 | line += ' '; 141 | 142 | if (kChunkText) { 143 | string += ' '; 144 | } 145 | } 146 | } 147 | 148 | line += ' ' + string; 149 | 150 | console.log(line); 151 | } 152 | } 153 | 154 | function print_instructions(indent, ip, instructions) { 155 | let bytes_per_line = 0; 156 | let crash_opcode = -1; 157 | 158 | for (let i = 0; i < instructions.length; ++i) { 159 | if (ip >= instructions[i].offset && (i === (instructions.length - 1) || ip < instructions[i + 1].offset)) { 160 | crash_opcode = i; 161 | break; 162 | } 163 | } 164 | 165 | if (crash_opcode >= 0) { 166 | for (let i = 0; i < instructions.length; ++i) { 167 | if (i < (crash_opcode - 5)) { 168 | continue; 169 | } 170 | 171 | if (i > (crash_opcode + 5)) { 172 | break; 173 | } 174 | 175 | const bytes = instructions[i].hex.length / 2; 176 | if (bytes > bytes_per_line) { 177 | bytes_per_line = bytes; 178 | } 179 | } 180 | } 181 | 182 | for (let i = 0; i < instructions.length; ++i) { 183 | if (crash_opcode >= 0) { 184 | if (i < (crash_opcode - 5)) { 185 | continue; 186 | } 187 | 188 | if (i > (crash_opcode + 5)) { 189 | break; 190 | } 191 | } 192 | 193 | let line = indent; 194 | 195 | if (crash_opcode >= 0 && i === crash_opcode) { 196 | line = ' >' + line.substr(3); 197 | } 198 | 199 | line += instructions[i].offset.toString(16).padStart(8, '0'); 200 | line += ' '; 201 | line += instructions[i].hex.match(/.{2}/g).join(' ').padEnd((bytes_per_line * 3) - 1, ' '); 202 | line += ' '; 203 | line += instructions[i].mnemonic; 204 | 205 | console.log(line); 206 | } 207 | } 208 | 209 | function print_thread(i, crashed, thread) { 210 | let title = 'Thread ' + i; 211 | if (crashed) { 212 | title += ' (crashed)'; 213 | } 214 | title += ':'; 215 | 216 | console.log(title); 217 | 218 | let num_frames = thread.length; 219 | /*if (num_frames > 10) { 220 | num_frames = 10; 221 | }*/ 222 | 223 | for (let i = 0; i < num_frames; ++i) { 224 | const frame = thread[i]; 225 | 226 | const prefix = i.toString().padStart((num_frames - 1).toString().length, ' ') + ': '; 227 | const indent = ' ' + ' '.repeat(prefix.length); 228 | 229 | console.log(' ' + prefix + frame.rendered); 230 | 231 | if (frame.url) { 232 | console.log(indent + frame.url); 233 | } 234 | 235 | if (frame.registers) { 236 | //console.log(indent + 'Registers'); 237 | print_registers(indent, frame.registers); 238 | console.log(''); 239 | } 240 | 241 | if (frame.instructions) { 242 | //console.log(indent + 'Disassembly'); 243 | print_instructions(indent, frame.instruction, frame.instructions); 244 | console.log(''); 245 | } 246 | 247 | if (frame.stack) { 248 | //console.log(indent + 'Stack Memory'); 249 | print_stack(indent, frame.registers && frame.registers.esp, base64ToUint8Array(frame.stack)); 250 | console.log(''); 251 | } 252 | 253 | console.log(indent + 'Found via ' + (FRAME_TRUST[frame.trust] || FRAME_TRUST[0])); 254 | console.log(''); 255 | 256 | console.log(''); 257 | } 258 | } 259 | 260 | function base64ToUint8Array(base64) { 261 | var binary_string = window.atob(base64); 262 | var len = binary_string.length; 263 | var bytes = new Uint8Array( len ); 264 | for (var i = 0; i < len; i++) { 265 | bytes[i] = binary_string.charCodeAt(i); 266 | } 267 | return bytes; 268 | } 269 | 270 | function analyze(data) { 271 | if (data.crashed) { 272 | console.log(data.crash_reason + ' accessing 0x' + data.crash_address.toString(16)); 273 | console.log(''); 274 | } 275 | 276 | if (typeof data.requesting_thread !== 'undefined' && data.requesting_thread >= 0) { 277 | const thread = data.threads[data.requesting_thread]; 278 | print_thread(data.requesting_thread, true, thread); 279 | } 280 | 281 | for (let i = 0; i < data.threads.length; ++i) { 282 | if (i === data.requesting_thread) { 283 | continue; 284 | } 285 | 286 | const thread = data.threads[i]; 287 | print_thread(i, false, thread); 288 | } 289 | } 290 | -------------------------------------------------------------------------------- /web/js/carburetor-memory.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | if (!String.prototype.padStart) { 4 | String.prototype.padStart = function padStart(targetLength,padString) { 5 | targetLength = targetLength>>0; //floor if number or convert non-number to 0; 6 | padString = String(padString || ' '); 7 | if (this.length > targetLength) { 8 | return String(this); 9 | } 10 | else { 11 | targetLength = targetLength-this.length; 12 | if (targetLength > padString.length) { 13 | padString += padString.repeat(targetLength/padString.length); //append to original to ensure we are longer than needed 14 | } 15 | return padString.slice(0,targetLength) + String(this); 16 | } 17 | }; 18 | } 19 | 20 | if (!String.prototype.padEnd) { 21 | String.prototype.padEnd = function padEnd(targetLength,padString) { 22 | targetLength = targetLength>>0; //floor if number or convert non-number to 0; 23 | padString = String(padString || ' '); 24 | if (this.length > targetLength) { 25 | return String(this); 26 | } 27 | else { 28 | targetLength = targetLength-this.length; 29 | if (targetLength > padString.length) { 30 | padString += padString.repeat(targetLength/padString.length); //append to original to ensure we are longer than needed 31 | } 32 | return String(this) + padString.slice(0,targetLength); 33 | } 34 | }; 35 | } 36 | 37 | function base64ToUint8Array(base64) { 38 | var binary_string = window.atob(base64); 39 | var len = binary_string.length; 40 | var bytes = new Uint8Array( len ); 41 | for (var i = 0; i < len; i++) { 42 | bytes[i] = binary_string.charCodeAt(i); 43 | } 44 | return bytes; 45 | } 46 | 47 | function print_memory(region) { 48 | const base = region.base; 49 | const memory = base64ToUint8Array(region.data); 50 | 51 | if (memory.length !== region.size) { 52 | throw new Error('size mismatch'); 53 | } 54 | 55 | const kAddressBytes = 4; 56 | const kHeader = false; 57 | const kRowBytes = 32; 58 | const kChunkBytes = 8; 59 | const kChunkText = false; 60 | 61 | if (kHeader) { 62 | let line = ' '.repeat(kAddressBytes * 2) + ' '; 63 | let string = ''; 64 | for (let i = 0; i < kRowBytes; ++i) { 65 | line += i.toString(16).padStart(2, ' ') + ' '; 66 | string += ' '; 67 | //string += i.toString(16).padStart(2, ' ').substr(1, 1); 68 | 69 | if (kChunkBytes > 0 && i < (kRowBytes - 1) && (i % kChunkBytes) === (kChunkBytes - 1)) { 70 | line += ' '; 71 | if (kChunkText) { 72 | string += ' '; 73 | } 74 | } 75 | } 76 | line += ' ' + string + ' '; 77 | console.log(line); 78 | } 79 | 80 | for (let offset = 0; offset < memory.length;) { 81 | let line = (base + offset).toString(16).padStart(kAddressBytes * 2, '0') + ' '; 82 | 83 | let string = ''; 84 | for (let i = 0; i < kRowBytes; ++i, ++offset) { 85 | if (offset < memory.length) { 86 | line += memory[offset].toString(16).padStart(2, '0') + ' '; 87 | 88 | const character = String.fromCharCode(memory[offset]); 89 | string += (character.length === 1 && character.match(/^[ -~]$/)) ? character : '.'; 90 | } else { 91 | line += ' '; 92 | string += ' '; 93 | } 94 | 95 | if (kChunkBytes > 0 && i < (kRowBytes - 1) && (i % kChunkBytes) === (kChunkBytes - 1)) { 96 | line += ' '; 97 | 98 | if (kChunkText) { 99 | string += ' '; 100 | } 101 | } 102 | } 103 | 104 | line += ' ' + string; 105 | 106 | console.log(line); 107 | } 108 | } 109 | 110 | function memory(data) { 111 | const memory = data.memory; 112 | if (!memory) { 113 | console.log('No memory regions in input'); 114 | throw e; 115 | } 116 | 117 | memory.sort((a, b) => (a.base - b.base)); 118 | 119 | const start = memory[0].base; 120 | const end = memory[memory.length - 1].base + memory[memory.length - 1].size; 121 | const size = end - start; 122 | const real = memory.map((a) => a.size).reduce((a, b) => (a + b), 0); 123 | 124 | //console.log(start.toString(16), end.toString(16), size, real); 125 | console.log('Got ' + real + ' bytes of memory covering ' + start.toString(16).padStart(8, '0') + ' to ' + end.toString(16).padStart(8, '0') + ' (' + (real / size) + '% coverage)') 126 | console.log(''); 127 | 128 | for (let i = 0; i < memory.length; ++i) { 129 | print_memory(memory[i]); 130 | console.log(''); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /web/js/highcharts.data.js: -------------------------------------------------------------------------------- 1 | /* 2 | Data plugin for Highcharts 3 | 4 | (c) 2012-2014 Torstein Honsi 5 | 6 | License: www.highcharts.com/license 7 | */ 8 | (function(g){var j=g.each,q=HighchartsAdapter.inArray,s=g.splat,l,n=function(a,b){this.init(a,b)};g.extend(n.prototype,{init:function(a,b){this.options=a;this.chartOptions=b;this.columns=a.columns||this.rowsToColumns(a.rows)||[];this.rawColumns=[];this.columns.length?this.dataFound():(this.parseCSV(),this.parseTable(),this.parseGoogleSpreadsheet())},getColumnDistribution:function(){var a=this.chartOptions,b=this.options,c=[],f=function(a){return(g.seriesTypes[a||"line"].prototype.pointArrayMap||[0]).length}, 9 | e=a&&a.chart&&a.chart.type,d=[],i=[],p,h;j(a&&a.series||[],function(a){d.push(f(a.type||e))});j(b&&b.seriesMapping||[],function(a){c.push(a.x||0)});c.length===0&&c.push(0);j(b&&b.seriesMapping||[],function(b){var c=new l,m,j=d[p]||f(e),o=g.seriesTypes[((a&&a.series||[])[p]||{}).type||e||"line"].prototype.pointArrayMap||["y"];c.addColumnReader(b.x,"x");for(m in b)b.hasOwnProperty(m)&&m!=="x"&&c.addColumnReader(b[m],m);for(h=0;h=e&&c<=d&&!t&&g!==""&&(g=b.split(h),j(g,function(a,b){b>=i&&b<=p&&(f[b-i]||(f[b-i]=[]),f[b-i][r]=a)}),r+=1)}),this.dataFound())},parseTable:function(){var a=this.options,b=a.table,c=this.columns,f=a.startRow||0,e=a.endRow||Number.MAX_VALUE,d=a.startColumn||0,i=a.endColumn|| 12 | Number.MAX_VALUE;b&&(typeof b==="string"&&(b=document.getElementById(b)),j(b.getElementsByTagName("tr"),function(a,b){b>=f&&b<=e&&j(a.children,function(a,e){if((a.tagName==="TD"||a.tagName==="TH")&&e>=d&&e<=i)c[e-d]||(c[e-d]=[]),c[e-d][b-f]=a.innerHTML})}),this.dataFound())},parseGoogleSpreadsheet:function(){var a=this,b=this.options,c=b.googleSpreadsheetKey,f=this.columns,e=b.startRow||0,d=b.endRow||Number.MAX_VALUE,i=b.startColumn||0,g=b.endColumn||Number.MAX_VALUE,h,j;c&&jQuery.ajax({dataType:"json", 13 | url:"https://spreadsheets.google.com/feeds/cells/"+c+"/"+(b.googleSpreadsheetWorksheet||"od6")+"/public/values?alt=json-in-script&callback=?",error:b.error,success:function(b){var b=b.feed.entry,c,l=b.length,o=0,n=0,k;for(k=0;k=i&&k<=g)f[k-i]=[],f[k-i].length=Math.min(n,d-e);for(k=0;k=i&&j<=g&&h>=e&&h<=d)f[j-i][h-e]=c.content.$t;a.dataFound()}})},findHeaderRow:function(){var a= 14 | 0;j(this.columns,function(b){b.isNumeric&&typeof b[0]!=="string"&&(a=null)});this.headerRow=a},trim:function(a){return typeof a==="string"?a.replace(/^\s+|\s+$/g,""):a},parseTypes:function(){for(var a=this.columns,b=this.rawColumns,c=a.length,f,e,d,i,g,h,j=[],l,m=this.chartOptions;c--;){f=a[c].length;b[c]=[];for(l=(g=q(c,this.valueCount.xColumns)!==-1)&&m&&m.xAxis&&s(m.xAxis)[0].type==="category";f--;)if(e=j[f]||a[c][f],d=parseFloat(e),i=b[c][f]=this.trim(e),l)a[c][f]=i;else if(i==d)a[c][f]=d,d>31536E6? 15 | a[c].isDatetime=!0:a[c].isNumeric=!0;else if(d=this.parseDate(e),g&&typeof d==="number"&&!isNaN(d)){if(j[f]=e,a[c][f]=d,a[c].isDatetime=!0,a[c][f+1]!==void 0){e=d>a[c][f+1];if(e!==h&&h!==void 0)this.alternativeFormat?(this.dateFormat=this.alternativeFormat,f=a[c].length,this.alternativeFormat=this.dateFormats[this.dateFormat].alternative):a[c].unsorted=!0;h=e}}else if(a[c][f]=i===""?null:i,f!==0&&(a[c].isDatetime||a[c].isNumeric))a[c].mixed=!0;g&&a[c].mixed&&(a[c]=b[c])}if(a[0].isDatetime&&h){b=typeof a[0][0]!== 16 | "number";for(c=0;c0;){h=new l;h.addColumnReader(0,"x");d=q(0,e);d!==-1&&e.splice(d,1);for(d=0;d0&&g[0].readers.length>0&&(h=a[g[0].readers[0].columnIndex],h!==void 0&&(h.isDatetime?b="datetime":h.isNumeric||(b="category")));if(b==="category")for(d=0;d=2&&(e=this.getReferencedColumnIndexes(),e.length>=2))e.shift(),e.sort(),this.name=a[e.shift()].name;return f};l.prototype.addColumnReader=function(a,b){this.readers.push({columnIndex:a,configName:b});if(!(b==="x"||b==="y"||b===void 0))this.pointIsArray=!1};l.prototype.getReferencedColumnIndexes=function(){var a,b=[],c;for(a=0;ad;d++)c[d]=255&a.charCodeAt(d);return c}function f(a){return a>=0&&31>a?1<d;d++)c[d]=b[d];return c};l.context2d=document.createElement("canvas").getContext("2d")}var m={Int8:1,Int16:2,Int32:4,Uint8:1,Uint16:2,Uint32:4,Float32:4,Float64:8};d.wrapBuffer=function(a){switch(typeof a){case"number":if(i.ArrayBuffer)a=new Uint8Array(a).buffer;else if(i.PixelData)a=l(a);else{a=new Array(a);for(var c=0;c=0){var d=g.fromNumber(a);b=d.lo,c=d.hi}else c=Math.floor(a/f(32)),b=a-c*f(32),c+=f(32);return new h(b,c)};var n=d.prototype={compatibility:i,_checkBounds:function(a,b,d){if("number"!=typeof a)throw new TypeError("Offset is not a number.");if("number"!=typeof b)throw new TypeError("Size is not a number.");if(0>b)throw new RangeError("Length is negative.");if(0>a||a+b>c(d,this.byteLength))throw new RangeError("Offsets are out of bounds.")},_action:function(a,b,d,e,f){return this._engineAction(a,b,c(d,this._offset),c(e,this._littleEndian),f)},_dataViewAction:function(a,b,c,d,e){return this._offset=c+m[a],b?this._view["get"+a](c,d):this._view["set"+a](c,e,d)},_arrayBufferAction:function(b,d,e,f,g){var h,i=m[b],j=a[b+"Array"];if(f=c(f,this._littleEndian),1===i||(this.byteOffset+e)%i===0&&f)return h=new j(this.buffer,this.byteOffset+e,1),this._offset=e+i,d?h[0]:h[0]=g;var k=new Uint8Array(d?this.getBytes(i,e,f,!0):i);return h=new j(k.buffer,0,1),d?h[0]:(h[0]=g,void this._setBytes(e,k,f))},_arrayAction:function(a,b,c,d,e){return b?this["_get"+a](c,d):this["_set"+a](c,e,d)},_getBytes:function(a,d,e){e=c(e,this._littleEndian),d=c(d,this._offset),a=c(a,this.byteLength-d),this._checkBounds(d,a),d+=this.byteOffset,this._offset=d-this.byteOffset+a;var f=this._isArrayBuffer?new Uint8Array(this.buffer,d,a):(this.buffer.slice||Array.prototype.slice).call(this.buffer,d,d+a);return e||1>=a?f:b(f).reverse()},getBytes:function(a,d,e,f){var g=this._getBytes(a,d,c(e,!0));return f?b(g):g},_setBytes:function(a,d,e){var f=d.length;if(0!==f){if(e=c(e,this._littleEndian),a=c(a,this._offset),this._checkBounds(a,f),!e&&f>1&&(d=b(d,!0).reverse()),a+=this.byteOffset,this._isArrayBuffer)new Uint8Array(this.buffer,a,f).set(d);else for(var g=0;f>g;g++)this.buffer[a+g]=d[g];this._offset=a-this.byteOffset+f}},setBytes:function(a,b,d){this._setBytes(a,b,c(d,!0))},getString:function(a,b,c){var d=this._getBytes(a,b,!0);if(c="utf8"===c?"utf-8":c||"binary",k&&"binary"!==c)return new k(c).decode(this._isArrayBuffer?d:new Uint8Array(d));var e="";a=d.length;for(var f=0;a>f;f++)e+=String.fromCharCode(d[f]);return"utf-8"===c&&(e=decodeURIComponent(escape(e))),e},setString:function(a,b,c){c="utf8"===c?"utf-8":c||"binary";var d;j&&"binary"!==c?d=new j(c).encode(b):("utf-8"===c&&(b=unescape(encodeURIComponent(b))),d=e(b)),this._setBytes(a,d,!0)},getChar:function(a){return this.getString(1,a)},setChar:function(a,b){this.setString(a,b)},tell:function(){return this._offset},seek:function(a){return this._checkBounds(a,0),this._offset=a},skip:function(a){return this.seek(this._offset+a)},slice:function(a,b,e){function f(a,b){return 0>a?a+b:a}return a=f(a,this.byteLength),b=f(c(b,this.byteLength),this.byteLength),e?new d(this.getBytes(b-a,a,!0,!0),void 0,void 0,this._littleEndian):new d(this.buffer,this.byteOffset+a,b-a,this._littleEndian)},alignBy:function(a){return this._bitOffset=0,1!==c(a,1)?this.skip(a-(this._offset%a||a)):this._offset},_getFloat64:function(a,b){var c=this._getBytes(8,a,b),d=1-2*(c[7]>>7),e=((c[7]<<1&255)<<3|c[6]>>4)-1023,g=(15&c[6])*f(48)+c[5]*f(40)+c[4]*f(32)+c[3]*f(24)+c[2]*f(16)+c[1]*f(8)+c[0];return 1024===e?0!==g?0/0:1/0*d:-1023===e?d*g*f(-1074):d*(1+g*f(-52))*f(e)},_getFloat32:function(a,b){var c=this._getBytes(4,a,b),d=1-2*(c[3]>>7),e=(c[3]<<1&255|c[2]>>7)-127,g=(127&c[2])<<16|c[1]<<8|c[0];return 128===e?0!==g?0/0:1/0*d:-127===e?d*g*f(-149):d*(1+g*f(-23))*f(e)},_get64:function(a,b,d){d=c(d,this._littleEndian),b=c(b,this._offset);for(var e=d?[0,4]:[4,0],f=0;2>f;f++)e[f]=this.getUint32(b+e[f],d);return this._offset=b+8,new a(e[0],e[1])},getInt64:function(a,b){return this._get64(h,a,b)},getUint64:function(a,b){return this._get64(g,a,b)},_getInt32:function(a,b){var c=this._getBytes(4,a,b);return c[3]<<24|c[2]<<16|c[1]<<8|c[0]},_getUint32:function(a,b){return this._getInt32(a,b)>>>0},_getInt16:function(a,b){return this._getUint16(a,b)<<16>>16},_getUint16:function(a,b){var c=this._getBytes(2,a,b);return c[1]<<8|c[0]},_getInt8:function(a){return this._getUint8(a)<<24>>24},_getUint8:function(a){return this._getBytes(1,a)[0]},_getBitRangeData:function(a,b){var d=(c(b,this._offset)<<3)+this._bitOffset,e=d+a,f=d>>>3,g=e+7>>>3,h=this._getBytes(g-f,f,!0),i=0;(this._bitOffset=7&e)&&(this._bitOffset-=8);for(var j=0,k=h.length;k>j;j++)i=i<<8|h[j];return{start:f,bytes:h,wideValue:i}},getSigned:function(a,b){var c=32-a;return this.getUnsigned(a,b)<>c},getUnsigned:function(a,b){var c=this._getBitRangeData(a,b).wideValue>>>-this._bitOffset;return 32>a?c&~(-1<b?1:0,j=~(-1<b&&(b=-b),0===b?(g=0,h=0):isNaN(b)?(g=2*j+1,h=1):1/0===b?(g=2*j+1,h=0):(g=Math.floor(Math.log(b)/Math.LN2),g>=k&&j>=g?(h=Math.floor((b*f(-g)-1)*f(c)),g+=j):(h=Math.floor(b/f(k-c)),g=0));for(var l=[];c>=8;)l.push(h%256),h=Math.floor(h/256),c-=8;for(g=g<=8;)l.push(255&g),g>>>=8,d-=8;l.push(i<>>8&255,b>>>16&255,b>>>24],c)},_setUint16:function(a,b,c){this._setBytes(a,[255&b,b>>>8&255],c)},_setUint8:function(a,b){this._setBytes(a,[255&b])},setUnsigned:function(a,b,c){var d=this._getBitRangeData(c,a),e=d.wideValue,f=d.bytes;e&=~(~(-1<c?b&~(-1<=0;g--)f[g]=255&e,e>>>=8;this._setBytes(d.start,f,!0)}};for(var o in m)!function(a){n["get"+a]=function(b,c){return this._action(a,!0,b,c)},n["set"+a]=function(b,c,d){this._action(a,!1,b,d,c)}}(o);n._setInt32=n._setUint32,n._setInt16=n._setUint16,n._setInt8=n._setUint8,n.setSigned=n.setUnsigned;for(var p in n)"set"===p.slice(0,3)&&!function(a){n["write"+a]=function(){Array.prototype.unshift.call(arguments,void 0),this["set"+a].apply(this,arguments)}}(p.slice(3));return d}); 2 | -------------------------------------------------------------------------------- /web/js/jquery.hoverIntent.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * hoverIntent r7 // 2013.03.11 // jQuery 1.9.1+ 3 | * http://cherne.net/brian/resources/jquery.hoverIntent.html 4 | * 5 | * You may use hoverIntent under the terms of the MIT license. 6 | * Copyright 2007, 2013 Brian Cherne 7 | */ 8 | (function(e){e.fn.hoverIntent=function(t,n,r){var i={interval:100,sensitivity:7,timeout:0};if(typeof t==="object"){i=e.extend(i,t)}else if(e.isFunction(n)){i=e.extend(i,{over:t,out:n,selector:r})}else{i=e.extend(i,{over:t,out:t,selector:n})}var s,o,u,a;var f=function(e){s=e.pageX;o=e.pageY};var l=function(t,n){n.hoverIntent_t=clearTimeout(n.hoverIntent_t);if(Math.abs(u-s)+Math.abs(a-o)