diff options
Diffstat (limited to 'modules-available/passthrough')
-rw-r--r-- | modules-available/passthrough/config.json | 7 | ||||
-rw-r--r-- | modules-available/passthrough/inc/passthrough.inc.php | 53 | ||||
-rw-r--r-- | modules-available/passthrough/install.inc.php | 9 | ||||
-rw-r--r-- | modules-available/passthrough/page.inc.php | 128 | ||||
-rw-r--r-- | modules-available/passthrough/permissions/permissions.json | 3 | ||||
-rw-r--r-- | modules-available/passthrough/templates/hardware-list.html | 125 |
6 files changed, 325 insertions, 0 deletions
diff --git a/modules-available/passthrough/config.json b/modules-available/passthrough/config.json new file mode 100644 index 00000000..3f65995f --- /dev/null +++ b/modules-available/passthrough/config.json @@ -0,0 +1,7 @@ +{ + "category": "main.settings-client", + "dependencies": [ + "statistics", + "locations" + ] +}
\ No newline at end of file diff --git a/modules-available/passthrough/inc/passthrough.inc.php b/modules-available/passthrough/inc/passthrough.inc.php new file mode 100644 index 00000000..524aea5e --- /dev/null +++ b/modules-available/passthrough/inc/passthrough.inc.php @@ -0,0 +1,53 @@ +<?php + +class Passthrough +{ + + public static function getGroupDropdown(array &$row): array + { + $out = []; + if ($row['class'] === '0300') { + foreach (['GPU', 'GVT'] as $id) { + $out[] = [ + 'ptid' => $id, + 'ptname' => $id, + 'selected' => ($row['@PASSTHROUGH'] === $id ? 'selected' : ''), + ]; + } + return $out; + } + static $list = false; + if ($list === false) { + $list = Database::queryKeyValueList("SELECT groupid, title FROM passthrough_group ORDER BY groupid"); + self::ensurePrepopulated($list); + } + $row['custom_groups'] = true; + foreach ($list as $id => $title) { + if ($id === 'GPU' || $id === 'GVT') + continue; + $item = ['ptid' => $id, 'ptname' => $id . ' (' . $title . ')']; + if ($row['@PASSTHROUGH'] === $id) { + $item['selected'] = 'selected'; + } + $out[] = $item; + } + return $out; + } + + private static function ensurePrepopulated(&$list) + { + $want = [ + 'GPU' => '[Special] GPU passthrough default group', + 'GVT' => '[Special] Intel GVT-g default group', + ]; + foreach ($want as $id => $title) { + if (!isset($list[$id])) { + Database::exec("INSERT INTO passthrough_group (groupid, title) VALUES (:id, :title) + ON DUPLICATE KEY UPDATE title = VALUES(title)", + ['id' => $id, 'title' => $title]); + $list[$id] = $title; + } + } + } + +}
\ No newline at end of file diff --git a/modules-available/passthrough/install.inc.php b/modules-available/passthrough/install.inc.php new file mode 100644 index 00000000..e08be38b --- /dev/null +++ b/modules-available/passthrough/install.inc.php @@ -0,0 +1,9 @@ +<?php + +$result[] = tableCreate('passthrough_group', " + `groupid` varchar(32) CHARACTER SET ascii DEFAULT NULL, + `title` varchar(200) NOT NULL, + PRIMARY KEY (`groupid`) +"); + +responseFromArray($result);
\ No newline at end of file diff --git a/modules-available/passthrough/page.inc.php b/modules-available/passthrough/page.inc.php new file mode 100644 index 00000000..3ab0696e --- /dev/null +++ b/modules-available/passthrough/page.inc.php @@ -0,0 +1,128 @@ +<?php + +class Page_Passthrough extends Page +{ + + protected function doPreprocess() + { + User::load(); + User::assertPermission('view'); + $action = Request::post('action'); + if ($action === 'save-hwlist') { + $this->saveHwList(); + } + if (Request::isPost()) { + Util::redirect('?do=passthrough'); + } + } + + private function saveHwList() + { + $newgroups = Request::post('newgroup', [], 'array'); + foreach ($newgroups as $id => $title) { + $id = strtoupper(preg_replace('/[^a-z0-9_\-]/i', '', $id)); + if (empty($id)) + continue; + Database::exec("INSERT IGNORE INTO passthrough_group (groupid, title) + VALUES (:group, :title)", + ['group' => $id, 'title' => $title]); + } + $groups = Request::post('ptgroup', Request::REQUIRED, 'array'); + $insert = []; + $delete = []; + foreach ($groups as $hwid => $group) { + if (empty($group)) { + $delete[] = $hwid; + } else { + $insert[] = [$hwid, '@PASSTHROUGH', $group]; + } + } + if (!empty($insert)) { + Database::exec("INSERT INTO statistic_hw_prop (hwid, prop, `value`) VALUES :list + ON DUPLICATE KEY UPDATE `value` = VALUES(`value`)", + ['list' => $insert]); + } + if (!empty($delete)) { + Database::exec("DELETE FROM statistic_hw_prop WHERE hwid IN (:list) AND prop = '@PASSTHROUGH'", ['list' => $delete]); + } + Message::addSuccess('list-updated'); + Util::redirect('?do=passthrough&show=hwlist'); + } + + /* + * + */ + + protected function doRender() + { + $show = Request::get('show'); + if ($show === 'hwlist') { + $this->showHardwareList(); + } else { + Util::redirect('?do=passthrough&show=hwlist'); + } + } + + private function showHardwareList() + { + $q = new HardwareQuery(HardwareInfo::PCI_DEVICE, null, false); + $q->addGlobalColumn('vendor'); + $q->addGlobalColumn('device'); + $q->addGlobalColumn('class'); + $q->addGlobalColumn('@PASSTHROUGH'); + $rows = []; + foreach ($q->query('shw.hwid') as $row) { + $row['ptlist'] = Passthrough::getGroupDropdown($row); + $rows[] = $row; + } + // Sort Graphics Cards first, rest by class, vendor, device + usort($rows, function ($row1, $row2) { + $a = $row1['class']; + $b = $row2['class']; + if ($a === $b) + return hexdec($row1['vendor'].$row1['device']) - hexdec($row2['vendor'] . $row2['device']); + if ($a === '0300') + return -1; + if ($b === '0300') + return 1; + return hexdec($a) - hexdec($b); + }); + $finalRows = []; + $missing = []; + $lastClass = ''; + foreach ($rows as $row) { + if ($row['class'] !== $lastClass) { + // Add class row header + $lastClass = $row['class']; + $finalRows[] = [ + 'class' => $row['class'], + 'class_name' => PciId::getPciId(PciId::DEVCLASS, $row['class'], true) ?: 'Unknown', + ]; + } + $row['vendor_name'] = PciId::getPciId(PciId::VENDOR, $row['vendor'] ?? ''); + $row['device_name'] = PciId::getPciId(PciId::DEVICE, $row['vendor'] . ':' . $row['device']); + $finalRows[] = $row; + // Build up query + if ($row['vendor_name'] === false) { + $missing[$row['vendor']] = true; + } + if ($row['device_name'] === false) { + $missing[$row['vendor'] . ':' . $row['device']] = true; + } + } + Render::addTemplate('hardware-list', [ + 'list' => $finalRows, + 'missing_ids' => json_encode(array_keys($missing)), + ]); + } + + /* + * + */ + + protected function doAjax() + { + // + } + +}
\ No newline at end of file diff --git a/modules-available/passthrough/permissions/permissions.json b/modules-available/passthrough/permissions/permissions.json new file mode 100644 index 00000000..d0932f1e --- /dev/null +++ b/modules-available/passthrough/permissions/permissions.json @@ -0,0 +1,3 @@ +{ + "view": false +}
\ No newline at end of file diff --git a/modules-available/passthrough/templates/hardware-list.html b/modules-available/passthrough/templates/hardware-list.html new file mode 100644 index 00000000..d331acb5 --- /dev/null +++ b/modules-available/passthrough/templates/hardware-list.html @@ -0,0 +1,125 @@ +<form method="post" action="?do=passthrough&show=hwlist"> + <input type="hidden" name="token" value="{{token}}"> + <table class="table"> + <thead> + <tr> + <th class="slx-smallcol">{{lang_deviceIdNumeric}}</th> + <th>{{lang_deviceName}}</th> + <th class="slx-smallcol">{{lang_useCount}}</th> + <th>{{lang_passthroughGroup}}</th> + </tr> + </thead> + <tbody> + {{#list}} + <tr> + {{#class_name}} + <td colspan="4"> + <span>{{class}}</span> – <strong>{{class_name}}</strong> + </td> + {{/class_name}} + {{^class_name}} + <td>{{vendor}}:{{device}}</td> + <td> + <table class="slx-ellipsis"> + <tr> + <td {{^device_name}}class="query-{{vendor}}-{{device}}"{{/device_name}}> + {{device_name}} + </td> + </tr> + </table> + <div class="small {{^vendor_name}}query-{{vendor}}{{/vendor_name}}"> + {{vendor_name}} + </div> + </td> + <td class="text-right">{{connected_count}}</td> + <td> + <select name="ptgroup[{{hwid}}]" class="form-control {{#custom_groups}}ptgroup-select{{/custom_groups}}"> + <option value="">{{lang_noPassthroughGroup}}</option> + {{#ptlist}} + <option value="{{ptid}}" {{selected}}>{{ptname}}</option> + {{/ptlist}} + </select> + </td> + {{/class_name}} + </tr> + {{/list}} + </tbody> + </table> + <div id="new-groups"></div> + <div class="buttonbar text-right"> + <button type="button" data-target="#add-group-form" data-toggle="modal" class="btn btn-default"> + <span class="glyphicon glyphicon-plus"></span> + {{lang_add}} + </button> + <button type="submit" name="action" value="save-hwlist" class="btn btn-success"> + <span class="glyphicon glyphicon-floppy-disk"></span> + {{lang_save}} + </button> + </div> +</form> + +<div class="modal fade" id="add-group-form" tabindex="-1" role="dialog"> + <div class="modal-dialog" role="document"> + <div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal">×</button> + <b>{{lang_addPassthroughGroup}}</b> + </div> + <div class="modal-body"> + <div class="form-group"> + <label for="group-id">{{lang_groupId}}</label> + <input type="text" name="group-id" id="group-id" class="form-control"> + </div> + <div class="form-group"> + <label for="group-title">{{lang_groupTitle}}</label> + <input type="text" name="group-title" id="group-title" class="form-control"> + </div> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-default" + data-dismiss="modal">{{lang_cancel}}</button> + <button id="add-group-button" type="button" class="btn btn-success" data-dismiss="modal"> + <span class="glyphicon glyphicon-plus"></span> + {{lang_addGroup}} + </button> + </div> + </div> + </div> +</div> + +<script> + document.addEventListener('DOMContentLoaded', function() { + $('#add-group-button').click(function () { + var gid = $('#group-id').val().replace(/[^a-zA-Z0-9_\-]/g, '').toUpperCase(); + var title = $('#group-title').val().trim(); + if (gid.length === 0) + return; + $('#new-groups').append($('<input type="hidden">') + .attr('name', 'newgroup[' + gid + ']') + .attr('value', title)); + $('.ptgroup-select').each(function() { + $(this).append($('<option>').attr('value', gid).text(gid + ' (' + title + ')')); + }); + }); + var missing = {{{missing_ids}}}; + var doQuery = function() { + if (missing && missing.length > 0) { + $.ajax({ + url: '?do=statistics', dataType: "json", method: "POST", data: { + token: TOKEN, + action: 'json-lookup', + list: missing.splice(0, 10) // Query 10 at a time max + } + }).done(function (data) { + if (!data) + return; + for (var k in data) { + $('.query-' + k.replace(':', '-')).text(data[k]); + } + doQuery(); + }); + } + } + doQuery(); + }); +</script>
\ No newline at end of file |